Compare commits

..

147 Commits

Author SHA1 Message Date
notzippy@gmail.com
6ecc0a7c0a release v1.0.0 2020-07-11 22:57:36 -07:00
Steve
d8117a33d3 Merge pull request #186 from notzippy/go-mod
Removed version update from Revel
2020-06-06 07:50:58 -07:00
notzippy@gmail.com
6371373eb5 Removed version update
Version control is maintained through go.mod file
Modified harness to only kill the application if not responded after 60 seconds in windows
2020-06-06 07:49:10 -07:00
Steve
28ac65f1c1 Merge pull request #185 from notzippy/go-mod
Go mod updates
2020-05-19 02:42:57 -07:00
notzippy@gmail.com
5070fb8be0 Fixed issue with new and run flag
Updated tests to run final test in non gopath, with new name
2020-05-19 02:23:18 -07:00
notzippy@gmail.com
904cfa2995 Added some informational messages while download 2020-05-18 12:43:00 -07:00
notzippy@gmail.com
223bd3b7c0 Added manual scan on packages in app folder
This allows for source code generation. Packages in <application>/app folder are scanned manually as opposed to the `packages.Load` scan which will fast fail on compile error, and leave you with go files with no syntax.
2020-05-18 11:47:01 -07:00
notzippy@gmail.com
4987ee8319 Added verbose logging to building / testing a no-vendor app
Removed section which raises an error when examining packages, we dont need to check for errors on foreign packages since we are importing only a slice of the data
2020-05-17 05:58:28 -07:00
notzippy@gmail.com
4bab4409b9 Updated Revel command
Added a check to see if harness had already started, saves a recompile on load
Added check to source info for local import renames
Removed the go/build check for path and just check existence of the path
Formatting updates
2020-05-13 22:26:05 -07:00
notzippy@gmail.com
741f49236a Updated scanner
Removed scanning all the import statements, this is not needed
Added recursive scan for revel import path to pick up testunits
2020-05-08 15:41:20 -07:00
Steve
60b88a42c9 Merge pull request #180 from notzippy/go-mod
Initial commit to go mod
2020-05-03 21:47:09 -07:00
notzippy@gmail.com
49eef29bb5 Build and Historic build updates
Modified GOPATH to not modify build with go.mod
Updated go.mod to version 1.12
Updated harness to setup listener before killing process
Updated notvendored flag to --no-vendor
Updated command_config to ensure no-vendor can be build
Added additional checks in source path lookup
2020-05-03 13:39:48 -07:00
notzippy@gmail.com
9d3a554bec Updates
Updated NotVendored flag
Updated travis matrix
Updated build log
2020-04-29 22:01:28 -07:00
notzippy@gmail.com
36bd6b944a Corrected flags 2020-04-29 21:48:11 -07:00
notzippy@gmail.com
1d9df256a0 Moved test cases to run last 2020-04-29 21:39:19 -07:00
notzippy@gmail.com
ad694c0fb0 Debug travis 2020-04-29 21:32:12 -07:00
notzippy@gmail.com
fb4b56513a Debug travis
Added verbose flag so we can see what is occurring,
Removed checkout for revel, not needed anymore
2020-04-29 21:13:00 -07:00
notzippy@gmail.com
20d5766eb6 Added gomod-flags
Added a gomod-flags parameter which allows you to run go mod commands on the go.mod file before the build is performed. This allows for development environments.
2020-04-29 17:05:39 -07:00
notzippy@gmail.com
0920905a0c Updated to build go 1.12 and up
Modified to use fsnotify directlyUpdated travis to not use go deps
2020-04-28 12:56:23 -07:00
notzippy@gmail.com
31cb64e496 Check-in of command_test,
remaps the go mod command to use the develop branch.
2020-04-27 09:07:04 -07:00
notzippy@gmail.com
33abc47c7a Fixed remaining test 2020-04-26 23:00:51 -07:00
notzippy@gmail.com
86736d6e43 Updated formating
Ran through testing individually for vendored Revel applications
2020-04-26 22:29:16 -07:00
notzippy@gmail.com
07d67846c1 Restructured command config
Removed go/build reference in clean
2020-04-26 22:28:46 -07:00
notzippy@gmail.com
c1aee24445 Corrected version detection, so that equal versions match 2020-04-26 22:28:46 -07:00
notzippy@gmail.com
f2b54f5a69 Updated sourceinfo
Added packagepathmap to the SourceInfo, this in turn allows the RevelCLI app command to pass the source paths directly to Revel directly
Added default to build to be "target" of the current folder
Renamed source processor
2020-04-26 22:28:46 -07:00
notzippy@gmail.com
3f54665d4e Added processor to read the functions in the imported files, and populate the SourceInfo object the same as before 2020-04-26 22:28:46 -07:00
notzippy@gmail.com
548cbc1764 Upatede Error type to SourceError
Added processor object to code
Verified compile errors appearing
Signed-off-by: notzippy@gmail.com
2020-04-26 22:28:46 -07:00
notzippy@gmail.com
9a9511d28f Updated so revel new works and revel run starts parsing the source. 2020-04-26 22:28:46 -07:00
notzippy@gmail.com
acb8fb631b Initial commit to go mod 2020-04-26 22:28:46 -07:00
Steve
d2014633af Merge pull request #176 from xXLokerXx/fix_windows_path
acept slash and inverted slash in src path validation
2020-04-13 07:32:55 -07:00
Steve
773f6889b4 Merge branch 'develop' into fix_windows_path 2020-04-13 07:32:03 -07:00
Steve
ca4cfa567e Merge pull request #165 from kumakichi/fixed_import_C
fixed import "C"
2020-04-13 07:30:30 -07:00
Steve
436869049c Merge pull request #179 from Laur1nMartins/Laur1nMartins/fix-linkerFlags
Fix linker flags inclusion in build command
2020-04-13 07:29:34 -07:00
Steve
cf2e617618 Merge branch 'develop' into Laur1nMartins/fix-linkerFlags 2020-04-13 07:29:15 -07:00
Laurin
424474a035 Fix linker flags inclusion in build comamnd 2020-02-10 10:35:43 +01:00
Steve
531aa1e209 Merge pull request #178 from helgix/package-bugfix
eliminates package bug on silent build problems
2019-09-30 16:54:18 -07:00
Олег Вакарев
83dfdb8ad2 this fixes error when revel package creates tar without indicating that build had errors due to unhandled error 2019-09-26 13:14:33 +03:00
xXlokerXx
6d8fcd90c1 Fix sintax error 2019-07-08 19:48:09 -05:00
xXlokerXx
aa459c1b66 Fix sintaxis error 2019-07-08 19:19:33 -05:00
xXlokerXx
0b23b3e494 Fix complexity 2019-07-08 18:38:10 -05:00
xXlokerXx
3f65e1ef41 acept slash and inverted slash in src path validation 2019-07-08 18:23:58 -05:00
san
7dce3d8967 fixed import "C" 2018-11-03 09:12:25 +08:00
Steve
149f9f992b Merge pull request #161 from notzippy/master
Patch for windows
2018-11-01 20:31:14 -07:00
NotZippy
dfb08d9bd2 Amended importPathFromPath to detect vendor folder from basePath, not just the word vendor in the path 2018-11-01 09:06:31 -07:00
NotZippy
98e771cd01 Patch for windows
Made interrupt call os.Kill for windows
Added check for process still running before killing it
2018-10-31 12:52:43 -07:00
NotZippy
5c8d5bca7f develop v1.0.0-dev 2018-10-30 06:23:53 -07:00
NotZippy
5f558aca4e release v0.21.0 2018-10-30 06:23:52 -07:00
Steve
facfe0ecaf Merge pull request #159 from notzippy/develop
Patchset for 21
2018-10-29 16:29:03 -07:00
NotZippy
ee53d2f399 Patchset for 21
Added Version -u to update the checked out libaraies
Enhanced new skeleton toto support http https and git schemas when cloning the skeleton repo.
Enhanced version command to fetch server version from master branch
Enhanced version command to update local repository if a new version exists on the server
2018-10-29 15:26:59 -07:00
Steve
3c48e1f83e Merge pull request #158 from hwsoderlund/fix_compile_error
Fix compilation error visibility
2018-10-29 10:19:17 -07:00
Henrik Söderlund
19fb7d6776 Fix compilation error visibility 2018-10-29 16:55:59 +01:00
Steve
1e7b5322f5 Merge pull request #157 from HaraldNordgren/go_versions
Bump Go versions and use '.x' to always get latest patch versions
2018-10-28 11:08:56 -07:00
Harald Nordgren
205c652f07 Bump Go versions and use '.x' to always get latest patch versions 2018-10-28 18:42:35 +01:00
Steve
3de8b8c03c Merge pull request #156 from notzippy/develop
Modified run command to translate symlinks to absolute paths
2018-10-25 09:19:43 -07:00
NotZippy
4a877b2a8a Modified run command to translate symlinks to absolute paths 2018-10-24 21:29:07 -07:00
Steve
a0bafdc2a7 Merge pull request #155 from notzippy/develop
Added ability to readback output from command line
2018-10-24 10:26:16 -07:00
NotZippy
cdef0b75a8 Added ability to readback output from command line
Updated readme
2018-10-24 10:23:09 -07:00
Steve
e0d3f83ca8 Merge pull request #153 from notzippy/develop
Updated tool to give more meaningful messages
2018-10-19 09:12:48 -07:00
NotZippy
87c9e56322 Tool updates
Updated tool to give more meaningful errors
Added file system as an option to fetch the skeleton from
2018-10-19 06:45:01 -07:00
Steve
554e62574d Merge pull request #152 from notzippy/develop
Allow windows to fail on travis since to address a bug on current master
2018-10-18 08:05:51 -07:00
NotZippy
32a3f08dde Allow windows to fail on travis since to address a bug on current master 2018-10-18 08:05:15 -07:00
Steve
e6e1cad795 Merge pull request #148 from notzippy/develop
Shutdown / logger update
2018-10-12 21:20:05 -07:00
NotZippy
5e36cb1025 Updated travis to use checkout matching branch of Revel Framework for build. 2018-10-12 20:50:42 -07:00
NotZippy
8c21a56302 Revel tool enhancements
* run Command will choose CWD if no additional arguments are supplied
* Added Revel version check, compatible lists are in model/version
2018-10-12 20:40:48 -07:00
NotZippy
031fde6009 Update to logger 2018-10-12 20:40:48 -07:00
Steve
09ca80add8 Merge pull request #151 from lujiacn/master
Update build.go
2018-10-12 15:48:22 -07:00
Jia Lu
2d6c2eefa4 Update build.go
add missing c.Build.ImportPath, which is required to generate run.sh and run.bat
2018-10-12 22:53:01 +08:00
Steve
644d6e12bd Added check for copy dir
CopyDir should not fail if the source folder does not exist.
2018-10-10 11:40:29 -07:00
Steve
be7bebd962 Merge pull request #146 from notzippy/develop
Added missing environment variables to command, skipping the gopath
2018-10-02 09:27:54 -07:00
NotZippy
43c188c1eb Added missing environment variables to command, skipping the gopath 2018-10-02 08:48:12 -07:00
Steve
f745fb3edf Merge pull request #145 from revel/develop
V 0.20.1 For revel/cmd
To address a lot of the automatic import issues.
Added in a number of test cases 
Refactored CI testing
2018-10-01 11:08:23 -07:00
Steve
1302671687 Merge pull request #144 from notzippy/develop
Moved skeleton to its own repository
2018-10-01 09:33:37 -07:00
NotZippy
9ad0065f6c Moved skeleton to its own repository 2018-10-01 08:58:25 -07:00
Steve
e5303e84cd Merge pull request #143 from notzippy/develop
Enhancements to Revel command
2018-09-30 10:08:35 -07:00
NotZippy
f4fb2ec091 Enhancements to Revel command
Reformat of code
Allow user to use a mix of command line arguments and flags
Enhance the import tool to detect missing packages in the modules side
Added test cases for all commands
2018-09-30 10:08:11 -07:00
Steve
b606ec999c Merge pull request #138 from notzippy/develop
Re added the requirement for the -a, without this the flags would not…
2018-09-22 15:32:30 -07:00
NotZippy
01ccd695d4 Re added the requirement for the -a, without this the flags would not error out and cause issues 2018-09-22 15:28:22 -07:00
Steve
cfe5bf4b0c Merge pull request #137 from notzippy/develop
Added a version file to revel/cmd
2018-09-22 13:38:17 -07:00
NotZippy
7a4e741d1c Added a version file to revel/cmd
Updated import path detection to make it smarter. You can now use absolute paths etc..
2018-09-22 13:37:27 -07:00
Steve
92943b2121 Merge pull request #136 from notzippy/develop
New Enhancement
2018-09-21 10:19:49 -07:00
NotZippy
69e59efb14 New Enhancement
Added ability to create a new revel applicaiton without any sources.
Automatically download all sources required
2018-09-21 10:08:37 -07:00
Steve
5973b438c1 Merge pull request #135 from notzippy/develop
Added message for debugging
2018-09-19 14:44:05 -07:00
NotZippy
c47f44762a Added message for debugging
Added process state message to be returned to wait channel to help resolve the reason for the "app died" message
2018-09-19 14:43:15 -07:00
Steve
b138e35f6d Merge pull request #134 from notzippy/develop
Split main file
2018-09-19 10:38:27 -07:00
NotZippy
17459d14e6 Split main file
Added code to split the generated main file into two separate files. This allows other code to launch the web application inline.
2018-09-19 09:47:51 -07:00
Steve
c87d53eafa Merge pull request #133 from notzippy/develop
Updated readme, Updated travis
2018-09-16 20:05:19 -07:00
NotZippy
4d7a290247 Updated readme, Updated travis 2018-09-16 20:04:40 -07:00
Steve
7eff69f3cb Merge pull request #130 from notzippy/develop
Added CI
2018-09-16 07:11:28 -07:00
NotZippy
34bc650ea8 Added CI 2018-09-16 06:54:57 -07:00
Steve
2c53671706 Merge pull request #129 from notzippy/develop
Command line update
2018-09-15 16:28:32 -07:00
NotZippy
3ad381d45b Enhanced package and build to by default not include any source code 2018-09-14 21:32:20 -07:00
NotZippy
d0baaeb9e9 Initial rewrite of revel/cmd to provide vendor support and enhanced CLI options 2018-09-14 21:26:25 -07:00
notzippy
d2ac018544 Merge pull request #120 from lokhman/issue119
Fix DefaultValidationKeys generated with wrong line for multiline check
2018-04-15 21:14:55 -07:00
notzippy
d0e5c797cb Merge pull request #122 from notzippy/develop
Added missed GPL license to command
2018-04-15 21:13:23 -07:00
NotZippy
7e501b8a65 Added missed GPL license to command 2018-04-15 21:09:59 -07:00
Alexander Lokhman
fe56bdd8a3 Fix DefaultValidationKeys generated with wrong line for multiline check 2018-02-12 15:37:35 +00:00
notzippy
97ec142262 Merge pull request #117 from tike/master
fix import path trimming during main.go generation
2018-02-04 16:32:57 -08:00
tike
dfc873bc15 fix import path trimming during main.go generation
The importPathFromPath function invoked during `revel build`
in callchain Build -> ProcessSource ->  importPathFromPath
assumes that the vendor folder is in the app's root directory
when trimming import paths for inclusion into autogenerated
templates.

Consequently vendor detection fails if the vendor folder
is located at another hiher layer in the directory tree
and /prefix/path/to/vendor/ is not stripped from the
import path, leading to inclusion of invalid importpaths,
resulting in compilation error and build abortion.

This fix makes the vendor folder detection more flexible,
allowing for the vendor folder to be present at any higher
level in the directory hirachy.
2018-02-01 15:25:13 +01:00
notzippy
cca02dd5ff Merge pull request #116 from notzippy/log-update
Added check to ignore functions which have no body (external functions)
2018-01-30 09:47:56 -08:00
NotZippy
91f43bf94c Added check to ignore functions which have no body (external functions)
Added missing sort package
2018-01-30 09:23:21 -08:00
notzippy
0583fe7d32 Merge pull request #108 from rokeller/develop
Generate same value of AppVersion regardless of where revel is run
2018-01-29 21:28:08 -08:00
notzippy
6ca1d73b61 Merge pull request #112 from nathantchan/stable_controllers
Sort controllers so that builds are reproducible.
2018-01-29 21:27:18 -08:00
notzippy
4c87861642 Merge pull request #114 from vin01/master
Adding referrer policy security header
2018-01-29 21:26:33 -08:00
notzippy
a2d7517ca0 Merge pull request #115 from runner-mei/master
add support to map as a argument in the controller action
2018-01-29 21:25:49 -08:00
meifakun
8efaff19ce map as a argument in the controller action 2018-01-15 16:07:46 +08:00
vin01
ac056d17af Adding referrer policy security header
It will set a default strict `Referrer-Policy ``strict-origin-when-cross-origin`` that controls what referrer information shall be included with requests.
More: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy, https://scotthelme.co.uk/a-new-security-header-referrer-policy/
It can prevent issues like: https://robots.thoughtbot.com/is-your-site-leaking-password-reset-links
2018-01-06 14:05:20 +05:30
Nathan Chan
fc904827cd Make sorting compatible with go >= 1.6 2017-12-11 09:55:26 -08:00
Nathan Chan
c240b05369 Sort controllers so that builds are reproducible.
Ordering of controllers in routes.go and main.go is unstable in
successive runs of revel build.  This change will assure that the
ordering is stable.
2017-12-08 11:37:28 -08:00
notzippy
29e594435c Merge pull request #104 from notzippy/log-update
Prerelease items
2017-10-31 08:12:16 -07:00
Roger Keller
a2acbe32bf Make sure AppVersion is set without the -dirty suffix for non-dirty work trees regardless of where revel is run from. 2017-10-28 21:20:28 +02:00
NotZippy
29c6237caf Removed the catch all route, and added comment about security issue 2017-10-22 09:31:18 -07:00
NotZippy
2d4ccf289c Set line number to use left parenthesis not right 2017-10-07 21:41:23 -07:00
notzippy
f38fb6a15d Merge pull request #103 from notzippy/log-update
Updated skeleton to added critical.
2017-10-07 21:41:05 -07:00
NotZippy
637ccbd250 Updated skeleton to added critical.
modified db.import to support multiple packages
2017-10-07 21:39:13 -07:00
notzippy
2da4734499 Merge pull request #101 from notzippy/log-update
Vendor changes
2017-09-25 09:10:40 -07:00
NotZippy
aa9e0f8600 Added code to make vendoring work 2017-09-25 07:36:48 -07:00
notzippy
db4054233b Merge pull request #99 from Acidic9/master
Remove abort with 'revel new' on empty directory
2017-09-24 13:17:32 -07:00
Ari Seyhun
3907c6575e Clean code 2017-09-23 14:20:09 +09:30
notzippy
27e9fab270 Merge pull request #100 from notzippy/log-update
Logging error added more compile warnings
2017-09-21 10:50:28 -07:00
NotZippy
17e7d40d31 Fixed missing debug context parameter name
Added check to see if specfication was not exported
Added warnings if expected types did not match specification
2017-09-20 17:49:34 -07:00
Ari Seyhun
54ce8d3699 Remove abort with 'revel new' on empty directory
If you use 'revel new ...' on an empty directory, revel will abort complaining the directory exists.

With this commit, it will no longer abort if the directory is empty.
2017-09-16 15:24:37 +09:30
notzippy
8ab98db556 Merge pull request #98 from notzippy/log-update
Updated command to use new logging
2017-09-14 17:16:59 -07:00
NotZippy
baf5e9f848 Added check to see if parameter was a local object, if so parse it 2017-09-14 17:15:22 -07:00
NotZippy
9d57681ae6 Updated command to use new logging 2017-09-02 09:10:21 -07:00
notzippy
3f136726db Merge pull request #97 from notzippy/listener-fix
Changed listener to be a pointer receiver
2017-08-25 15:55:14 -07:00
NotZippy
c0a515facf Changed listener to be a pointer receiver so setting the channel to nil actually persists 2017-08-24 21:48:49 -07:00
notzippy
01494f75fb Merge pull request #96 from notzippy/autorun
Added mutex lock on Refresh, removed check for app existence
2017-08-07 20:55:21 -07:00
NotZippy
e6b34786bb Added mutex lock on Refresh, removed check for app existence 2017-08-07 16:56:19 -07:00
notzippy
79b2afb5e5 Merge pull request #95 from notzippy/autorun
Made develop mode autorun on start
2017-08-03 20:00:26 -07:00
NotZippy
5fcde12193 Moved watcher inside harness
Modified proxy so application is launched on startup
2017-07-28 13:27:28 -07:00
notzippy
ad68773b9e Merge pull request #91 from notzippy/server-engine-2
Server Engine 2
2017-07-24 12:49:34 -07:00
NotZippy
e5255cd373 Updated as requested 2017-07-06 15:31:43 -07:00
NotZippy
3cf6d5094e Changed skeleton back to original 2017-06-07 09:45:09 -07:00
NotZippy
efcd02de37 Modified harness to bootstrap using the go engine. Skeleton app updated to use new request code 2017-06-07 09:45:09 -07:00
notzippy
7eda33eb71 Merge pull request #93 from notzippy/cmd-fix
Fixed captialization
2017-05-31 20:25:56 -07:00
NotZippy
1c5fb4a6f8 Fixed captialization 2017-05-31 20:25:04 -07:00
Brenden Soares
a699dab33d Merge pull request #61 from krhubert/develop
Use config.http.addr and config.http.ssl for create baseURL local server
2017-05-30 21:46:51 -07:00
NotZippy
e1776bda3c Rollback a change that was committed by mistake to develop branch 2017-04-26 21:04:12 -07:00
notzippy
d68b27ae81 Merge pull request #86 from tw4452852/version
fix version check against devel
2017-04-21 08:55:01 -07:00
Brenden Soares
ce84b78204 Merge pull request #85 from revel/app.conf-cleanup-1
Adding consistent values and example formatting
2017-04-15 18:57:56 -07:00
Tw
19ca52182d fix version check against devel
Signed-off-by: Tw <tw19881113@gmail.com>
2017-04-10 22:31:37 +08:00
Brenden Soares
bf30aab381 Adding consistent values and example formatting 2017-04-07 12:05:14 -07:00
notzippy
bd4663b651 Merge pull request #84 from revel/app.conf-cleanup-1
App.conf cleanup 1
2017-04-07 12:05:08 -07:00
Brenden Soares
b81860de5f Remove unneeded quotes 2017-04-07 11:34:44 -07:00
NotZippy
d2b1730439 Makes it so harness can bootstrap using the new GoRequest / response wrappers 2017-04-04 17:17:23 -07:00
krhubert
0381636044 Typo in httpProto 2016-08-17 10:20:13 +02:00
krhubert
fb3980ce9d Use config.http.addr and config.http.ssl for create baseURL test server 2016-08-11 11:41:48 +02:00
99 changed files with 7790 additions and 2374 deletions

13
.codebeatsettings Normal file
View File

@@ -0,0 +1,13 @@
{
"GOLANG": {
"ABC":[33, 38, 50, 70],
"ARITY":[5,6,7,8],
"BLOCK_NESTING":[9, 10, 12, 13],
"CYCLO":[30, 35, 45, 60],
"TOO_MANY_IVARS": [28, 30, 40, 45],
"TOO_MANY_FUNCTIONS": [20, 30, 40, 50],
"TOTAL_COMPLEXITY": [150, 250, 400, 500],
"LOC": [100, 175, 250, 320],
"TOTAL_LOC": [300, 400, 500, 600]
}
}

73
.travis.yml Normal file
View File

@@ -0,0 +1,73 @@
language: go
go:
- "1.12.x"
- "1.13.x"
- "1.14.x"
- "tip"
os:
- osx
- linux
- windows
sudo: false
branches:
only:
- master
- develop
env:
# Setting environments variables
- GO111MODULE=on
install:
- export PATH=$PATH:$HOME/gopath/bin
- export REVEL_BRANCH="develop"
- 'if [[ "$TRAVIS_BRANCH" == "master" ]]; then export REVEL_BRANCH="master"; fi'
- 'echo "Travis branch: $TRAVIS_BRANCH, Revel dependency branch: $REVEL_BRANCH"'
# Since travis already checks out go build the commandline tool (revel)
- mkdir $HOME/GOPATH_PROTECTED
- export GOPATH=$HOME/GOPATH_PROTECTED
- go build -o $HOME/gopath/bin/revel github.com/revel/cmd/revel
- pwd
- env
script:
- go test -v github.com/revel/cmd/revel/...
# Ensure the new-app flow works (plus the other commands).
#- revel version
#- revel new my/testapp
#- revel test my/testapp
#- revel clean my/testapp
#- revel build my/testapp build/testapp
#- revel build my/testapp build/testapp prod
#- revel package my/testapp
#- revel package my/testapp prod
# Ensure the new-app flow works (plus the other commands).
- revel new --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 --package revelframework.com -v
- revel test --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v
- revel clean --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v
- revel build --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v -t build/testapp2
- revel build --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v -t build/testapp2 -m prod
- revel package --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v
- revel package --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp2 -v -m prod
- export INITIALWD=$PWD
# Check build works with no-vendor flag
- cd $GOPATH
- export GO111MODULE=auto
- revel new -a my/testapp2 --no-vendor -v
- revel test -a my/testapp2 -v
# Check non verbose build, outside of GO path
- cd $INITIALWD
- revel new --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp3 --package revelframework.com
- revel test --gomod-flags "edit -replace=github.com/revel/revel=github.com/revel/revel@$REVEL_BRANCH" -a my/testapp3
matrix:
allow_failures:
- go: tip
- os: windows

20
LICENSE Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (C) 2012-2018 The Revel Framework Authors.
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.

View File

@@ -1,5 +1,9 @@
# Revel command line tools
[![Build Status](https://secure.travis-ci.org/revel/cmd.svg?branch=master)](http://travis-ci.org/revel/cmd)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Go Report Card](https://goreportcard.com/badge/github.com/revel/cmd)](https://goreportcard.com/report/github.com/revel/cmd)
Provides the `revel` command, used to create and run Revel apps.
- More info at http://revel.github.io/manual/tool.html
@@ -7,5 +11,41 @@ Provides the `revel` command, used to create and run Revel apps.
Install
------------
```bash
go get github.com/revel/cmd/revel
go get -u github.com/revel/cmd/revel
```
New Application
-------------
Create a new application
```commandline
revel new my/app
```
## Community
* [Gitter](https://gitter.im/revel/community)
* [StackOverflow](http://stackoverflow.com/questions/tagged/revel)
## Learn More
* [Manual, Samples, Godocs, etc](http://revel.github.io)
* [Apps using Revel](https://github.com/revel/revel/wiki/Apps-in-the-Wild)
* [Articles Featuring Revel](https://github.com/revel/revel/wiki/Articles)
## Contributing
* [Contributing Code Guidelines](https://github.com/revel/revel/blob/master/CONTRIBUTING.md)
* [Revel Contributors](https://github.com/revel/revel/graphs/contributors)
## Contributors
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/0)](https://sourcerer.io/fame/notzippy/revel/cmd/links/0)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/1)](https://sourcerer.io/fame/notzippy/revel/cmd/links/1)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/2)](https://sourcerer.io/fame/notzippy/revel/cmd/links/2)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/3)](https://sourcerer.io/fame/notzippy/revel/cmd/links/3)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/4)](https://sourcerer.io/fame/notzippy/revel/cmd/links/4)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/5)](https://sourcerer.io/fame/notzippy/revel/cmd/links/5)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/6)](https://sourcerer.io/fame/notzippy/revel/cmd/links/6)
[![](https://sourcerer.io/fame/notzippy/revel/cmd/images/7)](https://sourcerer.io/fame/notzippy/revel/cmd/links/7)

30
go.mod Normal file
View File

@@ -0,0 +1,30 @@
module github.com/revel/cmd
go 1.12
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/agtorre/gocolorize v1.0.0
github.com/fsnotify/fsnotify v1.4.7
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
github.com/jessevdk/go-flags v1.4.0
github.com/mattn/go-colorable v0.1.6
github.com/myesui/uuid v1.0.0 // indirect
github.com/pkg/errors v0.9.1
github.com/revel/config v0.21.0
github.com/revel/log15 v2.11.20+incompatible
github.com/revel/modules v0.21.0 // indirect
github.com/revel/pathtree v0.0.0-20140121041023-41257a1839e9 // indirect
github.com/revel/revel v0.21.0
github.com/stretchr/testify v1.4.0
github.com/twinj/uuid v1.0.0 // indirect
github.com/xeonx/timeago v1.0.0-rc4 // indirect
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 // indirect
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 // indirect
golang.org/x/tools v0.0.0-20200219054238-753a1d49df85
gopkg.in/fsnotify/fsnotify.v1 v1.4.7
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
gopkg.in/natefinch/lumberjack.v2 v2.0.0
gopkg.in/stack.v0 v0.0.0-20141108040640-9b43fcefddd0
gopkg.in/stretchr/testify.v1 v1.2.2 // indirect
)

60
go.sum Normal file
View File

@@ -0,0 +1,60 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/agtorre/gocolorize v1.0.0 h1:TvGQd+fAqWQlDjQxSKe//Y6RaxK+RHpEU9X/zPmHW50=
github.com/agtorre/gocolorize v1.0.0/go.mod h1:cH6imfTkHVBRJhSOeSeEZhB4zqEYSq0sXuIyehgZMIY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 h1:KUDFlmBg2buRWNzIcwLlKvfcnujcHQRQ1As1LoaCLAM=
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/myesui/uuid v1.0.0/go.mod h1:2CDfNgU0LR8mIdO8vdWd8i9gWWxLlcoIGGpSNgafq84=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/revel/config v0.21.0 h1:Bw4iXLGAuD/Di2HEhPSOyDywrTlFIXUMbds91lXTtTU=
github.com/revel/config v0.21.0/go.mod h1:GT4a9px5kDGRqLizcw/md0QFErrhen76toz4qS3oIoI=
github.com/revel/log15 v2.11.20+incompatible h1:JkA4tbwIo/UGEMumY50zndKq816RQW3LQ0wIpRc+32U=
github.com/revel/log15 v2.11.20+incompatible/go.mod h1:l0WmLRs+IM1hBl4noJiBc2tZQiOgZyXzS1mdmFt+5Gc=
github.com/revel/modules v0.21.0/go.mod h1:UBlNmO9VGZo4j6Ptn2uC/26Iclefuic+V40jYRPBxQE=
github.com/revel/pathtree v0.0.0-20140121041023-41257a1839e9/go.mod h1:TmlwoRLDvgRjoTe6rbsxIaka/CulzYrgfef7iNJcEWY=
github.com/revel/revel v0.21.0/go.mod h1:VZWJnHjpDEtuGUuZJ2NO42XryitrtwsdVaJxfDeo5yc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/twinj/uuid v1.0.0/go.mod h1:mMgcE1RHFUFqe5AfiwlINXisXfDGro23fWdPUfOMjRY=
github.com/xeonx/timeago v1.0.0-rc4/go.mod h1:qDLrYEFynLO7y5Ho7w3GwgtYgpy5UfhcXIIQvMKVDkA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200219054238-753a1d49df85 h1:XNHaQ2CZDl/SjEZlUXGh7+OQvfLuFgmk3oNWkCFfERE=
golang.org/x/tools v0.0.0-20200219054238-753a1d49df85/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify/fsnotify.v1 v1.4.7/go.mod h1:Fyux9zXlo4rWoMSIzpn9fDAYjalPqJ/K1qJ27s+7ltE=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/stack.v0 v0.0.0-20141108040640-9b43fcefddd0 h1:lMH45EKqD8Nf6LwoF+43YOKjOAEEHQRVgDyG8RCV4MU=
gopkg.in/stack.v0 v0.0.0-20141108040640-9b43fcefddd0/go.mod h1:kl/bNzW/jgTgUOCGDj3XPn9/Hbfhw6pjfBRUnaTioFQ=
gopkg.in/stretchr/testify.v1 v1.2.2/go.mod h1:QI5V/q6UbPmuhtm10CaFZxED9NreB8PnFYN9JcR6TxU=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -12,26 +12,31 @@ import (
"os"
"os/exec"
"time"
"sync"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"runtime"
)
// App contains the configuration for running a Revel app. (Not for the app itself)
// Its only purpose is constructing the command to execute.
type App struct {
BinaryPath string // Path to the app executable
Port int // Port to pass as a command line argument.
cmd AppCmd // The last cmd returned.
BinaryPath string // Path to the app executable
Port int // Port to pass as a command line argument.
cmd AppCmd // The last cmd returned.
PackagePathMap map[string]string // Package to directory path map
Paths *model.RevelContainer
}
// NewApp returns app instance with binary path in it
func NewApp(binPath string) *App {
return &App{BinaryPath: binPath}
func NewApp(binPath string, paths *model.RevelContainer, packagePathMap map[string]string) *App {
return &App{BinaryPath: binPath, Paths: paths, Port: paths.HTTPPort, PackagePathMap:packagePathMap}
}
// Cmd returns a command to run the app server using the current configuration.
func (a *App) Cmd() AppCmd {
a.cmd = NewAppCmd(a.BinaryPath, a.Port)
func (a *App) Cmd(runMode string) AppCmd {
a.cmd = NewAppCmd(a.BinaryPath, a.Port, runMode, a.Paths)
return a.cmd
}
@@ -47,81 +52,167 @@ type AppCmd struct {
}
// NewAppCmd returns the AppCmd with parameters initialized for running app
func NewAppCmd(binPath string, port int) AppCmd {
func NewAppCmd(binPath string, port int, runMode string, paths *model.RevelContainer) AppCmd {
cmd := exec.Command(binPath,
fmt.Sprintf("-port=%d", port),
fmt.Sprintf("-importPath=%s", revel.ImportPath),
fmt.Sprintf("-runMode=%s", revel.RunMode))
fmt.Sprintf("-importPath=%s", paths.ImportPath),
fmt.Sprintf("-runMode=%s", runMode))
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
return AppCmd{cmd}
}
// Start the app server, and wait until it is ready to serve requests.
func (cmd AppCmd) Start() error {
listeningWriter := startupListeningWriter{os.Stdout, make(chan bool)}
func (cmd AppCmd) Start(c *model.CommandConfig) error {
listeningWriter := &startupListeningWriter{os.Stdout, make(chan bool), c, &bytes.Buffer{}}
cmd.Stdout = listeningWriter
revel.TRACE.Println("Exec app:", cmd.Path, cmd.Args)
utils.CmdInit(cmd.Cmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args, "dir", cmd.Dir, "env", cmd.Env)
if err := cmd.Cmd.Start(); err != nil {
revel.ERROR.Fatalln("Error running:", err)
utils.Logger.Fatal("Error running:", "error", err)
}
select {
case <-cmd.waitChan():
return errors.New("revel/harness: app died")
case exitState := <-cmd.waitChan():
fmt.Println("Startup failure view previous messages, \n Proxy is listening :", c.Run.Port)
err := utils.NewError("", "Revel Run Error", "starting your application there was an exception. See terminal output, " + exitState, "")
// TODO pretiffy command line output
// err.MetaError = listeningWriter.getLastOutput()
return err
case <-time.After(30 * time.Second):
case <-time.After(60 * time.Second):
println("Revel proxy is listening, point your browser to :", c.Run.Port)
utils.Logger.Error("Killing revel server process did not respond after wait timeout.", "processid", cmd.Process.Pid)
cmd.Kill()
return errors.New("revel/harness: app timed out")
case <-listeningWriter.notifyReady:
println("Revel proxy is listening, point your browser to :", c.Run.Port)
return nil
}
// TODO remove this unreachable code and document it
panic("Impossible")
}
// Run the app server inline. Never returns.
func (cmd AppCmd) Run() {
revel.TRACE.Println("Exec app:", cmd.Path, cmd.Args)
utils.Logger.Info("Exec app:", "path", cmd.Path, "args", cmd.Args)
if err := cmd.Cmd.Run(); err != nil {
revel.ERROR.Fatalln("Error running:", err)
utils.Logger.Fatal("Error running:", "error", err)
}
}
// Kill terminates the app server if it's running.
func (cmd AppCmd) Kill() {
if cmd.Cmd != nil && (cmd.ProcessState == nil || !cmd.ProcessState.Exited()) {
revel.TRACE.Println("Killing revel server pid", cmd.Process.Pid)
err := cmd.Process.Kill()
if err != nil {
revel.ERROR.Fatalln("Failed to kill revel server:", err)
// Windows appears to send the kill to all threads, shutting down the
// server before this can, this check will ensure the process is still running
if _, err := os.FindProcess(int(cmd.Process.Pid)); err != nil {
// Server has already exited
utils.Logger.Info("Server not running revel server pid", "pid", cmd.Process.Pid)
return
}
// Wait for the shutdown channel
waitMutex := &sync.WaitGroup{}
waitMutex.Add(1)
ch := make(chan bool, 1)
go func() {
waitMutex.Done()
s, err := cmd.Process.Wait()
defer func() {
ch <- true
}()
if err != nil {
utils.Logger.Info("Wait failed for process ", "error", err)
}
if s != nil {
utils.Logger.Info("Revel App exited", "state", s.String())
}
}()
// Wait for the channel to begin waiting
waitMutex.Wait()
// Send an interrupt signal to allow for a graceful shutdown
utils.Logger.Info("Killing revel server pid", "pid", cmd.Process.Pid)
var err error
if runtime.GOOS != "windows" {
// os.Interrupt is not available on windows
err = cmd.Process.Signal(os.Interrupt)
}
if err != nil {
utils.Logger.Info(
"Revel app already exited.",
"processid", cmd.Process.Pid, "error", err,
"killerror", cmd.Process.Kill())
return
}
// Use a timer to ensure that the process exits
utils.Logger.Info("Waiting to exit")
select {
case <-ch:
return
case <-time.After(60 * time.Second):
// Kill the process
utils.Logger.Error(
"Revel app failed to exit in 60 seconds - killing.",
"processid", cmd.Process.Pid,
"killerror", cmd.Process.Kill())
}
utils.Logger.Info("Done Waiting to exit")
}
}
// Return a channel that is notified when Wait() returns.
func (cmd AppCmd) waitChan() <-chan struct{} {
ch := make(chan struct{}, 1)
func (cmd AppCmd) waitChan() <-chan string {
ch := make(chan string, 1)
go func() {
_ = cmd.Wait()
ch <- struct{}{}
state := cmd.ProcessState
exitStatus := " unknown "
if state != nil {
exitStatus = state.String()
}
ch <- exitStatus
}()
return ch
}
// A io.Writer that copies to the destination, and listens for "Listening on.."
// A io.Writer that copies to the destination, and listens for "Revel engine is listening on.."
// in the stream. (Which tells us when the revel server has finished starting up)
// This is super ghetto, but by far the simplest thing that should work.
type startupListeningWriter struct {
dest io.Writer
notifyReady chan bool
c *model.CommandConfig
buffer *bytes.Buffer
}
func (w startupListeningWriter) Write(p []byte) (n int, err error) {
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening")) {
// Writes to this output stream
func (w *startupListeningWriter) Write(p []byte) (int, error) {
if w.notifyReady != nil && bytes.Contains(p, []byte("Revel engine is listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
if w.c.HistoricMode {
if w.notifyReady != nil && bytes.Contains(p, []byte("Listening on")) {
w.notifyReady <- true
w.notifyReady = nil
}
}
if w.notifyReady != nil {
w.buffer.Write(p)
}
return w.dest.Write(p)
}
// Returns the cleaned output from the response
// TODO clean the response more
func (w *startupListeningWriter) getLastOutput() string {
return w.buffer.String()
}

377
harness/build.go Executable file → Normal file
View File

@@ -13,62 +13,97 @@ import (
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"text/template"
"time"
"github.com/revel/revel"
"github.com/revel/cmd/model"
_ "github.com/revel/cmd/parser"
"github.com/revel/cmd/utils"
"github.com/revel/cmd/parser2"
"github.com/revel/cmd/parser"
)
var importErrorPattern = regexp.MustCompile("cannot find package \"([^\"]+)\"")
type ByString []*model.TypeInfo
func (c ByString) Len() int {
return len(c)
}
func (c ByString) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c ByString) Less(i, j int) bool {
return c[i].String() < c[j].String()
}
// Build the app:
// 1. Generate the the main.go file.
// 2. Run the appropriate "go build" command.
// Requires that revel.Init has been called previously.
// Returns the path to the built binary, and an error if there was a problem building it.
func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
func Build(c *model.CommandConfig, paths *model.RevelContainer) (_ *App, err error) {
// First, clear the generated files (to avoid them messing with ProcessSource).
cleanSource("tmp", "routes")
cleanSource(paths, "tmp", "routes")
sourceInfo, compileError := ProcessSource(revel.CodePaths)
if compileError != nil {
return nil, compileError
var sourceInfo *model.SourceInfo
if c.HistoricBuildMode {
sourceInfo, err = parser.ProcessSource(paths)
} else {
sourceInfo, err = parser2.ProcessSource(paths)
}
if err != nil {
return
}
// Add the db.import to the import paths.
if dbImportPath, found := revel.Config.String("db.import"); found {
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, dbImportPath)
if dbImportPath, found := paths.Config.String("db.import"); found {
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, strings.Split(dbImportPath, ",")...)
}
// Sort controllers so that file generation is reproducible
controllers := sourceInfo.ControllerSpecs()
sort.Stable(ByString(controllers))
// Generate two source files.
templateArgs := map[string]interface{}{
"Controllers": sourceInfo.ControllerSpecs(),
"ImportPath": paths.ImportPath,
"Controllers": controllers,
"ValidationKeys": sourceInfo.ValidationKeys,
"ImportPaths": calcImportAliases(sourceInfo),
"TestSuites": sourceInfo.TestSuites(),
}
genSource("tmp", "main.go", RevelMainTemplate, templateArgs)
genSource("routes", "routes.go", RevelRoutesTemplate, templateArgs)
// Generate code for the main, run and routes file.
// The run file allows external programs to launch and run the application
// without being the main thread
cleanSource(paths, "tmp", "routes")
if err = genSource(paths, "tmp", "main.go", RevelMainTemplate, templateArgs); err != nil {
return
}
if err = genSource(paths, filepath.Join("tmp", "run"), "run.go", RevelRunTemplate, templateArgs); err != nil {
return
}
if err = genSource(paths, "routes", "routes.go", RevelRoutesTemplate, templateArgs); err != nil {
return
}
// Read build config.
buildTags := revel.Config.StringDefault("build.tags", "")
buildTags := paths.Config.StringDefault("build.tags", "")
// Build the user program (all code under app).
// It relies on the user having "go" installed.
goPath, err := exec.LookPath("go")
if err != nil {
revel.ERROR.Fatalf("Go executable not found in PATH.")
utils.Logger.Fatal("Go executable not found in PATH.")
}
pkg, err := build.Default.Import(revel.ImportPath, "", build.FindOnly)
if err != nil {
revel.ERROR.Fatalln("Failure importing", revel.ImportPath)
}
// Binary path is a combination of $GOBIN/revel.d directory, app's import path and its name.
binName := filepath.Join(pkg.BinDir, "revel.d", revel.ImportPath, filepath.Base(revel.BasePath))
// Binary path is a combination of target/app directory, app's import path and its name.
binName := filepath.Join("target", "app", paths.ImportPath, filepath.Base(paths.BasePath))
// Change binary path for Windows build
goos := runtime.GOOS
@@ -80,70 +115,129 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
}
gotten := make(map[string]struct{})
contains := func(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
if len(c.GoModFlags) > 0 {
for _, gomod := range c.GoModFlags {
goModCmd := exec.Command(goPath, append([]string{"mod"}, strings.Split(gomod, " ")...)...)
utils.CmdInit(goModCmd, !c.Vendored, c.AppPath)
output, err := goModCmd.CombinedOutput()
utils.Logger.Info("Gomod applied ", "output", string(output))
// If the build succeeded, we're done.
if err != nil {
utils.Logger.Error("Gomod Failed continuing ", "error", err, "output", string(output))
}
}
}
for {
appVersion := getAppVersion()
appVersion := getAppVersion(paths)
if appVersion == "" {
appVersion = "noVersionProvided"
}
buildTime := time.Now().UTC().Format(time.RFC3339)
versionLinkerFlags := fmt.Sprintf("-X %s/app.AppVersion=%s -X %s/app.BuildTime=%s",
revel.ImportPath, appVersion, revel.ImportPath, buildTime)
versionLinkerFlags := fmt.Sprintf("-X '%s/app.AppVersion=%s' -X '%s/app.BuildTime=%s'",
paths.ImportPath, appVersion, paths.ImportPath, buildTime)
// TODO remove version check for versionLinkerFlags after Revel becomes Go min version to go1.5
goVersion, _ := strconv.ParseFloat(runtime.Version()[2:5], 64)
if goVersion < 1.5 {
versionLinkerFlags = fmt.Sprintf("-X %s/app.AppVersion \"%s\" -X %s/app.BuildTime \"%s\"",
revel.ImportPath, appVersion, revel.ImportPath, buildTime)
// Append any build flags specified, they will override existing flags
flags := []string{}
if len(c.BuildFlags) == 0 {
flags = []string{
"build",
"-ldflags", versionLinkerFlags,
"-tags", buildTags,
"-o", binName}
} else {
if !contains(c.BuildFlags, "build") {
flags = []string{"build"}
}
flags = append(flags, c.BuildFlags...)
if !contains(flags, "-ldflags") {
ldflags := "-ldflags= " + versionLinkerFlags
// Add in build flags
for i := range c.BuildFlags {
ldflags += "-X '" + c.BuildFlags[i] + "'"
}
flags = append(flags, ldflags)
}
if !contains(flags, "-tags") {
flags = append(flags, "-tags", buildTags)
}
if !contains(flags, "-o") {
flags = append(flags, "-o", binName)
}
}
flags := []string{
"build",
"-i",
"-ldflags", versionLinkerFlags,
"-tags", buildTags,
"-o", binName}
// Add in build flags
flags = append(flags, buildFlags...)
flags = append(flags, c.BuildFlags...)
// This is Go main path
// Note: It's not applicable for filepath.* usage
flags = append(flags, path.Join(revel.ImportPath, "app", "tmp"))
flags = append(flags, path.Join(paths.ImportPath, "app", "tmp"))
buildCmd := exec.Command(goPath, flags...)
revel.TRACE.Println("Exec:", buildCmd.Args)
if !c.Vendored {
// This is Go main path
gopath := c.GoPath
for _, o := range paths.ModulePathMap {
gopath += string(filepath.ListSeparator) + o.Path
}
buildCmd.Env = append(os.Environ(),
"GOPATH=" + gopath,
)
}
utils.CmdInit(buildCmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec:", "args", buildCmd.Args, "working dir", buildCmd.Dir)
output, err := buildCmd.CombinedOutput()
// If the build succeeded, we're done.
if err == nil {
return NewApp(binName), nil
utils.Logger.Info("Build successful continuing")
return NewApp(binName, paths, sourceInfo.PackageMap), nil
}
revel.ERROR.Println(string(output))
// Since there was an error, capture the output in case we need to report it
stOutput := string(output)
utils.Logger.Infof("Got error on build of app %s", stOutput)
// See if it was an import error that we can go get.
matches := importErrorPattern.FindStringSubmatch(string(output))
matches := importErrorPattern.FindAllStringSubmatch(stOutput, -1)
utils.Logger.Info("Build failed checking for missing imports", "message", stOutput, "missing_imports", len(matches))
if matches == nil {
return nil, newCompileError(output)
utils.Logger.Info("Build failed no missing imports", "message", stOutput)
return nil, newCompileError(paths, output)
}
// Ensure we haven't already tried to go get it.
pkgName := matches[1]
if _, alreadyTried := gotten[pkgName]; alreadyTried {
return nil, newCompileError(output)
}
gotten[pkgName] = struct{}{}
// Execute "go get <pkg>"
getCmd := exec.Command(goPath, "get", pkgName)
revel.TRACE.Println("Exec:", getCmd.Args)
getOutput, err := getCmd.CombinedOutput()
if err != nil {
revel.ERROR.Println(string(getOutput))
return nil, newCompileError(output)
utils.Logger.Warn("Detected missing packages, importing them", "packages", len(matches))
for _, match := range matches {
// Ensure we haven't already tried to go get it.
pkgName := match[1]
utils.Logger.Info("Trying to import ", "package", pkgName)
if _, alreadyTried := gotten[pkgName]; alreadyTried {
utils.Logger.Error("Failed to import ", "package", pkgName)
return nil, newCompileError(paths, output)
}
gotten[pkgName] = struct{}{}
if err := c.PackageResolver(pkgName); err != nil {
utils.Logger.Error("Unable to resolve package", "package", pkgName, "error", err)
return nil, newCompileError(paths, []byte(err.Error()))
}
}
// Success getting the import, attempt to build again.
}
// TODO remove this unreachable code and document it
revel.ERROR.Fatalf("Not reachable")
utils.Logger.Fatal("Not reachable")
return nil, nil
}
@@ -153,7 +247,7 @@ func Build(buildFlags ...string) (app *App, compileError *revel.Error) {
// variable
// - Read the output of "git describe" if the source is in a git repository
// If no version can be determined, an empty string is returned.
func getAppVersion() string {
func getAppVersion(paths *model.RevelContainer) string {
if version := os.Getenv("APP_VERSION"); version != "" {
return version
}
@@ -161,17 +255,17 @@ func getAppVersion() string {
// Check for the git binary
if gitPath, err := exec.LookPath("git"); err == nil {
// Check for the .git directory
gitDir := filepath.Join(revel.BasePath, ".git")
gitDir := filepath.Join(paths.BasePath, ".git")
info, err := os.Stat(gitDir)
if (err != nil && os.IsNotExist(err)) || !info.IsDir() {
return ""
}
gitCmd := exec.Command(gitPath, "--git-dir="+gitDir, "describe", "--always", "--dirty")
revel.TRACE.Println("Exec:", gitCmd.Args)
gitCmd := exec.Command(gitPath, "--git-dir=" + gitDir, "--work-tree=" + paths.BasePath, "describe", "--always", "--dirty")
utils.Logger.Info("Exec:", "args", gitCmd.Args)
output, err := gitCmd.Output()
if err != nil {
revel.WARN.Println("Cannot determine git repository version:", err)
utils.Logger.Error("Cannot determine git repository version:", "error", err)
return ""
}
@@ -181,19 +275,19 @@ func getAppVersion() string {
return ""
}
func cleanSource(dirs ...string) {
func cleanSource(paths *model.RevelContainer, dirs ...string) {
for _, dir := range dirs {
cleanDir(dir)
cleanDir(paths, dir)
}
}
func cleanDir(dir string) {
revel.INFO.Println("Cleaning dir " + dir)
tmpPath := filepath.Join(revel.AppPath, dir)
func cleanDir(paths *model.RevelContainer, dir string) {
utils.Logger.Info("Cleaning dir ", "dir", dir)
tmpPath := filepath.Join(paths.AppPath, dir)
f, err := os.Open(tmpPath)
if err != nil {
if !os.IsNotExist(err) {
revel.ERROR.Println("Failed to clean dir:", err)
utils.Logger.Error("Failed to clean dir:", "error", err)
}
} else {
defer func() {
@@ -203,20 +297,20 @@ func cleanDir(dir string) {
infos, err := f.Readdir(0)
if err != nil {
if !os.IsNotExist(err) {
revel.ERROR.Println("Failed to clean dir:", err)
utils.Logger.Fatal("Failed to clean dir:", "error", err)
}
} else {
for _, info := range infos {
path := filepath.Join(tmpPath, info.Name())
pathName := filepath.Join(tmpPath, info.Name())
if info.IsDir() {
err := os.RemoveAll(path)
err := os.RemoveAll(pathName)
if err != nil {
revel.ERROR.Println("Failed to remove dir:", err)
utils.Logger.Fatal("Failed to remove dir:", "error", err)
}
} else {
err := os.Remove(path)
err := os.Remove(pathName)
if err != nil {
revel.ERROR.Println("Failed to remove file:", err)
utils.Logger.Fatal("Failed to remove file:", "error", err)
}
}
}
@@ -226,39 +320,17 @@ func cleanDir(dir string) {
// genSource renders the given template to produce source code, which it writes
// to the given directory and file.
func genSource(dir, filename, templateSource string, args map[string]interface{}) {
sourceCode := revel.ExecuteTemplate(
template.Must(template.New("").Parse(templateSource)),
args)
func genSource(paths *model.RevelContainer, dir, filename, templateSource string, args map[string]interface{}) error {
// Create a fresh dir.
cleanSource(dir)
tmpPath := filepath.Join(revel.AppPath, dir)
err := os.Mkdir(tmpPath, 0777)
if err != nil && !os.IsExist(err) {
revel.ERROR.Fatalf("Failed to make '%v' directory: %v", dir, err)
}
// Create the file
file, err := os.Create(filepath.Join(tmpPath, filename))
if err != nil {
revel.ERROR.Fatalf("Failed to create file: %v", err)
}
defer func() {
_ = file.Close()
}()
if _, err = file.WriteString(sourceCode); err != nil {
revel.ERROR.Fatalf("Failed to write to file: %v", err)
}
return utils.GenerateTemplate(filepath.Join(paths.AppPath, dir, filename), templateSource, args)
}
// Looks through all the method args and returns a set of unique import paths
// that cover all the method arg types.
// Additionally, assign package aliases when necessary to resolve ambiguity.
func calcImportAliases(src *SourceInfo) map[string]string {
func calcImportAliases(src *model.SourceInfo) map[string]string {
aliases := make(map[string]string)
typeArrays := [][]*TypeInfo{src.ControllerSpecs(), src.TestSuites()}
typeArrays := [][]*model.TypeInfo{src.ControllerSpecs(), src.TestSuites()}
for _, specs := range typeArrays {
for _, spec := range specs {
addAlias(aliases, spec.ImportPath, spec.PackageName)
@@ -285,6 +357,7 @@ func calcImportAliases(src *SourceInfo) map[string]string {
return aliases
}
// Adds an alias to the map of alias names
func addAlias(aliases map[string]string, importPath, pkgName string) {
alias, ok := aliases[importPath]
if ok {
@@ -294,16 +367,18 @@ func addAlias(aliases map[string]string, importPath, pkgName string) {
aliases[importPath] = alias
}
// Generates a package alias
func makePackageAlias(aliases map[string]string, pkgName string) string {
i := 0
alias := pkgName
for containsValue(aliases, alias) {
for containsValue(aliases, alias) || alias == "revel" {
alias = fmt.Sprintf("%s%d", pkgName, i)
i++
}
return alias
}
// Returns true if this value is in the map
func containsValue(m map[string]string, val string) bool {
for _, v := range m {
if v == val {
@@ -315,15 +390,15 @@ func containsValue(m map[string]string, val string) bool {
// Parse the output of the "go build" command.
// Return a detailed Error.
func newCompileError(output []byte) *revel.Error {
func newCompileError(paths *model.RevelContainer, output []byte) *utils.SourceError {
errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`).
FindSubmatch(output)
if errorMatch == nil {
errorMatch = regexp.MustCompile(`(?m)^(.*?)\:(\d+)\:\s(.*?)$`).FindSubmatch(output)
errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch(output)
if errorMatch == nil {
revel.ERROR.Println("Failed to parse build errors:\n", string(output))
return &revel.Error{
utils.Logger.Error("Failed to parse build errors", "error", string(output))
return &utils.SourceError{
SourceType: "Go code",
Title: "Go Compilation Error",
Description: "See console for build error.",
@@ -332,16 +407,32 @@ func newCompileError(output []byte) *revel.Error {
errorMatch = append(errorMatch, errorMatch[3])
revel.ERROR.Println("Build errors:\n", string(output))
utils.Logger.Error("Build errors", "errors", string(output))
}
findInPaths := func(relFilename string) string {
// Extract the paths from the gopaths, and search for file there first
gopaths := filepath.SplitList(build.Default.GOPATH)
for _, gp := range gopaths {
newPath := filepath.Join(gp, "src", paths.ImportPath, relFilename)
println(newPath)
if utils.Exists(newPath) {
return newPath
}
}
newPath, _ := filepath.Abs(relFilename)
utils.Logger.Warn("Could not find in GO path", "file", relFilename)
return newPath
}
// Read the source for the offending file.
var (
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
absFilename, _ = filepath.Abs(relFilename)
line, _ = strconv.Atoi(string(errorMatch[2]))
description = string(errorMatch[4])
compileError = &revel.Error{
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
absFilename = findInPaths(relFilename)
line, _ = strconv.Atoi(string(errorMatch[2]))
description = string(errorMatch[4])
compileError = &utils.SourceError{
SourceType: "Go code",
Title: "Go Compilation Error",
Path: relFilename,
@@ -350,16 +441,16 @@ func newCompileError(output []byte) *revel.Error {
}
)
errorLink := revel.Config.StringDefault("error.link", "")
errorLink := paths.Config.StringDefault("error.link", "")
if errorLink != "" {
compileError.SetLink(errorLink)
}
fileStr, err := revel.ReadLines(absFilename)
fileStr, err := utils.ReadLines(absFilename)
if err != nil {
compileError.MetaError = absFilename + ": " + err.Error()
revel.ERROR.Println(compileError.MetaError)
utils.Logger.Info("Unable to readlines " + compileError.MetaError, "error", err)
return compileError
}
@@ -368,11 +459,13 @@ func newCompileError(output []byte) *revel.Error {
}
// RevelMainTemplate template for app/tmp/main.go
const RevelMainTemplate = `// GENERATED CODE - DO NOT EDIT
package main
const RevelRunTemplate = `// GENERATED CODE - DO NOT EDIT
// This file is the run file for Revel.
// It registers all the controllers and provides details for the Revel server engine to
// properly inject parameters directly into the action endpoints.
package run
import (
"flag"
"reflect"
"github.com/revel/revel"{{range $k, $v := $.ImportPaths}}
{{$v}} "{{$k}}"{{end}}
@@ -380,19 +473,19 @@ import (
)
var (
runMode *string = flag.String("runMode", "", "Run mode.")
port *int = flag.Int("port", 0, "By default, read from app.conf")
importPath *string = flag.String("importPath", "", "Go Import Path for the app.")
srcPath *string = flag.String("srcPath", "", "Path to the source root.")
// So compiler won't complain if the generated code doesn't reference reflect package...
_ = reflect.Invalid
)
func main() {
flag.Parse()
revel.Init(*runMode, *importPath, *srcPath)
revel.INFO.Println("Running revel server")
// Register and run the application
func Run(port int) {
Register()
revel.Run(port)
}
// Register all the controllers
func Register() {
revel.AppLog.Info("Running revel server")
{{range $i, $c := .Controllers}}
revel.RegisterController((*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),
[]*revel.MethodType{
@@ -418,13 +511,39 @@ func main() {
testing.TestSuites = []interface{}{ {{range .TestSuites}}
(*{{index $.ImportPaths .ImportPath}}.{{.StructName}})(nil),{{end}}
}
}
`
const RevelMainTemplate = `// GENERATED CODE - DO NOT EDIT
// This file is the main file for Revel.
// It registers all the controllers and provides details for the Revel server engine to
// properly inject parameters directly into the action endpoints.
package main
revel.Run(*port)
import (
"flag"
"{{.ImportPath}}/app/tmp/run"
"github.com/revel/revel"
)
var (
runMode *string = flag.String("runMode", "", "Run mode.")
port *int = flag.Int("port", 0, "By default, read from app.conf")
importPath *string = flag.String("importPath", "", "Go Import Path for the app.")
srcPath *string = flag.String("srcPath", "", "Path to the source root.")
)
func main() {
flag.Parse()
revel.Init(*runMode, *importPath, *srcPath)
run.Run(*port)
}
`
// RevelRoutesTemplate template for app/conf/routes
const RevelRoutesTemplate = `// GENERATED CODE - DO NOT EDIT
// This file provides a way of creating URL's based on all the actions
// found in all the controllers.
package routes
import "github.com/revel/revel"

View File

@@ -16,6 +16,7 @@ package harness
import (
"crypto/tls"
"fmt"
"time"
"go/build"
"io"
"net"
@@ -28,11 +29,16 @@ import (
"strings"
"sync/atomic"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"github.com/revel/cmd/watcher"
"html/template"
"io/ioutil"
"sync"
"encoding/json"
)
var (
watcher *revel.Watcher
doNotWatch = []string{"tmp", "views", "routes"}
lastRequestHadError int32
@@ -41,16 +47,76 @@ var (
// Harness reverse proxies requests to the application server.
// It builds / runs / rebuilds / restarts the server when code is changed.
type Harness struct {
app *App
serverHost string
port int
proxy *httputil.ReverseProxy
app *App // The application
useProxy bool // True if proxy is in use
serverHost string // The proxy server host
port int // The proxy serber port
proxy *httputil.ReverseProxy // The proxy
watcher *watcher.Watcher // The file watched
mutex *sync.Mutex // A mutex to prevent concurrent updates
paths *model.RevelContainer // The Revel container
config *model.CommandConfig // The configuration
runMode string // The runmode the harness is running in
isError bool // True if harness is in error state
ranOnce bool // True app compiled once
}
func renderError(w http.ResponseWriter, r *http.Request, err error) {
req, resp := revel.NewRequest(r), revel.NewResponse(w)
c := revel.NewController(req, resp)
c.RenderError(err).Apply(req, resp)
func (h *Harness) renderError(iw http.ResponseWriter, ir *http.Request, err error) {
// Render error here
// Grab the template from three places
// 1) Application/views/errors
// 2) revel_home/views/errors
// 3) views/errors
if err == nil {
utils.Logger.Panic("Caller passed in a nil error")
}
templateSet := template.New("__root__")
seekViewOnPath := func(view string) (path string) {
path = filepath.Join(h.paths.ViewsPath, "errors", view)
if !utils.Exists(path) {
path = filepath.Join(h.paths.RevelPath, "templates", "errors", view)
}
data, err := ioutil.ReadFile(path)
if err != nil {
utils.Logger.Error("Unable to read template file", path)
}
_, err = templateSet.New("errors/" + view).Parse(string(data))
if err != nil {
utils.Logger.Error("Unable to parse template file", path)
}
return
}
target := []string{seekViewOnPath("500.html"), seekViewOnPath("500-dev.html")}
if !utils.Exists(target[0]) {
fmt.Fprintf(iw, "Target template not found not found %s<br />\n", target[0])
fmt.Fprintf(iw, "An error ocurred %s", err.Error())
return
}
var revelError *utils.SourceError
switch e := err.(type) {
case *utils.SourceError:
revelError = e
case error:
revelError = &utils.SourceError{
Title: "Server Error",
Description: e.Error(),
}
}
if revelError == nil {
panic("no error provided")
}
viewArgs := map[string]interface{}{}
viewArgs["RunMode"] = h.paths.RunMode
viewArgs["DevMode"] = h.paths.DevMode
viewArgs["Error"] = revelError
// Render the template from the file
err = templateSet.ExecuteTemplate(iw, "errors/500.html", viewArgs)
if err != nil {
utils.Logger.Error("Failed to execute", "error", err)
}
}
// ServeHTTP handles all requests.
@@ -63,18 +129,23 @@ func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Flush any change events and rebuild app if necessary.
// Render an error page if the rebuild / restart failed.
err := watcher.Notify()
err := h.watcher.Notify()
if err != nil {
// In a thread safe manner update the flag so that a request for
// /favicon.ico does not trigger a rebuild
atomic.CompareAndSwapInt32(&lastRequestHadError, 0, 1)
renderError(w, r, err)
h.renderError(w, r, err)
return
}
// In a thread safe manner update the flag so that a request for
// /favicon.ico is allowed
atomic.CompareAndSwapInt32(&lastRequestHadError, 1, 0)
// Reverse proxy the request.
// (Need special code for websockets, courtesy of bradfitz)
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
proxyWebsocket(w, r, h.serverHost)
h.proxyWebsocket(w, r, h.serverHost)
} else {
h.proxy.ServeHTTP(w, r)
}
@@ -82,24 +153,27 @@ func (h *Harness) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewHarness method returns a reverse proxy that forwards requests
// to the given port.
func NewHarness() *Harness {
func NewHarness(c *model.CommandConfig, paths *model.RevelContainer, runMode string, noProxy bool) *Harness {
// Get a template loader to render errors.
// Prefer the app's views/errors directory, and fall back to the stock error pages.
revel.MainTemplateLoader = revel.NewTemplateLoader(
[]string{filepath.Join(revel.RevelPath, "templates")})
if err := revel.MainTemplateLoader.Refresh(); err != nil {
revel.ERROR.Println(err)
}
//revel.MainTemplateLoader = revel.NewTemplateLoader(
// []string{filepath.Join(revel.RevelPath, "templates")})
//if err := revel.MainTemplateLoader.Refresh(); err != nil {
// revel.RevelLog.Error("Template loader error", "error", err)
//}
addr := revel.HTTPAddr
port := revel.Config.IntDefault("harness.port", 0)
addr := paths.HTTPAddr
port := paths.Config.IntDefault("harness.port", 0)
scheme := "http"
if revel.HTTPSsl {
if paths.HTTPSsl {
scheme = "https"
}
// If the server is running on the wildcard address, use "localhost"
if addr == "" {
utils.Logger.Warn("No http.addr specified in the app.conf listening on localhost interface only. " +
"This will not allow external access to your application")
addr = "localhost"
}
@@ -109,38 +183,94 @@ func NewHarness() *Harness {
serverURL, _ := url.ParseRequestURI(fmt.Sprintf(scheme+"://%s:%d", addr, port))
harness := &Harness{
serverHarness := &Harness{
port: port,
serverHost: serverURL.String()[len(scheme+"://"):],
proxy: httputil.NewSingleHostReverseProxy(serverURL),
mutex: &sync.Mutex{},
paths: paths,
useProxy: !noProxy,
config: c,
runMode: runMode,
}
if revel.HTTPSsl {
harness.proxy.Transport = &http.Transport{
if paths.HTTPSsl {
serverHarness.proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
}
return harness
return serverHarness
}
// Refresh method rebuilds the Revel application and run it on the given port.
func (h *Harness) Refresh() (err *revel.Error) {
// called by the watcher
func (h *Harness) Refresh() (err *utils.SourceError) {
t := time.Now();
fmt.Println("Changed detected, recompiling")
err = h.refresh()
if err!=nil && !h.ranOnce && h.useProxy {
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
fmt.Printf("\nError compiling code, to view error details see proxy running on http://%s\n\n",addr)
}
h.ranOnce = true
fmt.Printf("\nTime to recompile %s\n",time.Now().Sub(t).String())
return
}
func (h *Harness) refresh() (err *utils.SourceError) {
// Allow only one thread to rebuild the process
// If multiple requests to rebuild are queued only the last one is executed on
// So before a build is started we wait for a second to determine if
// more requests for a build are triggered.
// Once no more requests are triggered the build will be processed
h.mutex.Lock()
defer h.mutex.Unlock()
if h.app != nil {
h.app.Kill()
}
revel.TRACE.Println("Rebuild")
h.app, err = Build()
if err != nil {
utils.Logger.Info("Rebuild Called")
var newErr error
h.app, newErr = Build(h.config, h.paths)
if newErr != nil {
utils.Logger.Error("Build detected an error", "error", newErr)
if castErr, ok := newErr.(*utils.SourceError); ok {
return castErr
}
err = &utils.SourceError{
Title: "App failed to start up",
Description: err.Error(),
}
return
}
h.app.Port = h.port
if err2 := h.app.Cmd().Start(); err2 != nil {
return &revel.Error{
Title: "App failed to start up",
Description: err2.Error(),
if h.useProxy {
h.app.Port = h.port
runMode := h.runMode
if !h.config.HistoricMode {
// Recalulate run mode based on the config
var paths []byte
if len(h.app.PackagePathMap)>0 {
paths, _ = json.Marshal(h.app.PackagePathMap)
}
runMode = fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, h.app.Paths.RunMode, h.config.Verbose, string(paths))
}
if err2 := h.app.Cmd(runMode).Start(h.config); err2 != nil {
utils.Logger.Error("Could not start application", "error", err2)
if err,k :=err2.(*utils.SourceError);k {
return err
}
return &utils.SourceError{
Title: "App failed to start up",
Description: err2.Error(),
}
}
} else {
h.app = nil
}
return
@@ -149,11 +279,11 @@ func (h *Harness) Refresh() (err *revel.Error) {
// WatchDir method returns false to file matches with doNotWatch
// otheriwse true
func (h *Harness) WatchDir(info os.FileInfo) bool {
return !revel.ContainsString(doNotWatch, info.Name())
return !utils.ContainsString(doNotWatch, info.Name())
}
// WatchFile method returns true given filename HasSuffix of ".go"
// otheriwse false
// otheriwse false - implements revel.DiscerningListener
func (h *Harness) WatchFile(filename string) bool {
return strings.HasSuffix(filename, ".go")
}
@@ -162,37 +292,48 @@ func (h *Harness) WatchFile(filename string) bool {
// server, which it runs and rebuilds as necessary.
func (h *Harness) Run() {
var paths []string
if revel.Config.BoolDefault("watch.gopath", false) {
if h.paths.Config.BoolDefault("watch.gopath", false) {
gopaths := filepath.SplitList(build.Default.GOPATH)
paths = append(paths, gopaths...)
}
paths = append(paths, revel.CodePaths...)
watcher = revel.NewWatcher()
watcher.Listen(h, paths...)
paths = append(paths, h.paths.CodePaths...)
h.watcher = watcher.NewWatcher(h.paths, false)
h.watcher.Listen(h, paths...)
go h.Refresh()
// h.watcher.Notify()
go func() {
addr := fmt.Sprintf("%s:%d", revel.HTTPAddr, revel.HTTPPort)
revel.INFO.Printf("Listening on %s", addr)
if h.useProxy {
go func() {
// Check the port to start on a random port
if h.paths.HTTPPort == 0 {
h.paths.HTTPPort = getFreePort()
}
addr := fmt.Sprintf("%s:%d", h.paths.HTTPAddr, h.paths.HTTPPort)
utils.Logger.Infof("Proxy server is listening on %s", addr)
var err error
if revel.HTTPSsl {
err = http.ListenAndServeTLS(
addr,
revel.HTTPSslCert,
revel.HTTPSslKey,
h)
} else {
err = http.ListenAndServe(addr, h)
}
if err != nil {
revel.ERROR.Fatalln("Failed to start reverse proxy:", err)
}
}()
// Kill the app on signal.
var err error
if h.paths.HTTPSsl {
err = http.ListenAndServeTLS(
addr,
h.paths.HTTPSslCert,
h.paths.HTTPSslKey,
h)
} else {
err = http.ListenAndServe(addr, h)
}
if err != nil {
utils.Logger.Error("Failed to start reverse proxy:", "error", err)
}
}()
}
// Make a new channel to listen for the interrupt event
ch := make(chan os.Signal)
signal.Notify(ch, os.Interrupt, os.Kill)
<-ch
// Kill the app and exit
if h.app != nil {
h.app.Kill()
}
@@ -203,25 +344,25 @@ func (h *Harness) Run() {
func getFreePort() (port int) {
conn, err := net.Listen("tcp", ":0")
if err != nil {
revel.ERROR.Fatal(err)
utils.Logger.Fatal("Unable to fetch a freee port address", "error", err)
}
port = conn.Addr().(*net.TCPAddr).Port
err = conn.Close()
if err != nil {
revel.ERROR.Fatal(err)
utils.Logger.Fatal("Unable to close port", "error", err)
}
return port
}
// proxyWebsocket copies data between websocket client and server until one side
// closes the connection. (ReverseProxy doesn't work with websocket requests.)
func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
func (h *Harness) proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
var (
d net.Conn
err error
)
if revel.HTTPSsl {
if h.paths.HTTPSsl {
// since this proxy isn't used in production,
// it's OK to set InsecureSkipVerify to true
// no need to add another configuration option.
@@ -231,7 +372,7 @@ func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
}
if err != nil {
http.Error(w, "Error contacting backend server.", 500)
revel.ERROR.Printf("Error dialing websocket backend %s: %v", host, err)
utils.Logger.Error("Error dialing websocket backend ", "host", host, "error", err)
return
}
hj, ok := w.(http.Hijacker)
@@ -241,21 +382,21 @@ func proxyWebsocket(w http.ResponseWriter, r *http.Request, host string) {
}
nc, _, err := hj.Hijack()
if err != nil {
revel.ERROR.Printf("Hijack error: %v", err)
utils.Logger.Error("Hijack error", "error", err)
return
}
defer func() {
if err = nc.Close(); err != nil {
revel.ERROR.Println(err)
utils.Logger.Error("Connection close error", "error", err)
}
if err = d.Close(); err != nil {
revel.ERROR.Println(err)
utils.Logger.Error("Dial close error", "error", err)
}
}()
err = r.Write(d)
if err != nil {
revel.ERROR.Printf("Error copying request to target: %v", err)
utils.Logger.Error("Error copying request to target", "error", err)
return
}

View File

@@ -1,796 +0,0 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package harness
// This file handles the app code introspection.
// It catalogs the controllers, their methods, and their arguments.
import (
"go/ast"
"go/build"
"go/parser"
"go/scanner"
"go/token"
"log"
"os"
"path/filepath"
"strings"
"github.com/revel/revel"
)
// SourceInfo is the top-level struct containing all extracted information
// about the app source code, used to generate main.go.
type SourceInfo struct {
// StructSpecs lists type info for all structs found under the code paths.
// They may be queried to determine which ones (transitively) embed certain types.
StructSpecs []*TypeInfo
// ValidationKeys provides a two-level lookup. The keys are:
// 1. The fully-qualified function name,
// e.g. "github.com/revel/examples/chat/app/controllers.(*Application).Action"
// 2. Within that func's file, the line number of the (overall) expression statement.
// e.g. the line returned from runtime.Caller()
// The result of the lookup the name of variable being validated.
ValidationKeys map[string]map[int]string
// A list of import paths.
// Revel notices files with an init() function and imports that package.
InitImportPaths []string
// controllerSpecs lists type info for all structs found under
// app/controllers/... that embed (directly or indirectly) revel.Controller
controllerSpecs []*TypeInfo
// testSuites list the types that constitute the set of application tests.
testSuites []*TypeInfo
}
// TypeInfo summarizes information about a struct type in the app source code.
type TypeInfo struct {
StructName string // e.g. "Application"
ImportPath string // e.g. "github.com/revel/examples/chat/app/controllers"
PackageName string // e.g. "controllers"
MethodSpecs []*MethodSpec
// Used internally to identify controllers that indirectly embed *revel.Controller.
embeddedTypes []*embeddedTypeName
}
// methodCall describes a call to c.Render(..)
// It documents the argument names used, in order to propagate them to RenderArgs.
type methodCall struct {
Path string // e.g. "myapp/app/controllers.(*Application).Action"
Line int
Names []string
}
// MethodSpec holds the information of one Method
type MethodSpec struct {
Name string // Name of the method, e.g. "Index"
Args []*MethodArg // Argument descriptors
RenderCalls []*methodCall // Descriptions of Render() invocations from this Method.
}
// MethodArg holds the information of one argument
type MethodArg struct {
Name string // Name of the argument.
TypeExpr TypeExpr // The name of the type, e.g. "int", "*pkg.UserType"
ImportPath string // If the arg is of an imported type, this is the import path.
}
type embeddedTypeName struct {
ImportPath, StructName string
}
// Maps a controller simple name (e.g. "Login") to the methods for which it is a
// receiver.
type methodMap map[string][]*MethodSpec
// ProcessSource parses the app controllers directory and
// returns a list of the controller types found.
// Otherwise CompileError if the parsing fails.
func ProcessSource(roots []string) (*SourceInfo, *revel.Error) {
var (
srcInfo *SourceInfo
compileError *revel.Error
)
for _, root := range roots {
rootImportPath := importPathFromPath(root)
if rootImportPath == "" {
revel.WARN.Println("Skipping code path", root)
continue
}
// Start walking the directory tree.
_ = revel.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Println("Error scanning app source:", err)
return nil
}
if !info.IsDir() || info.Name() == "tmp" {
return nil
}
// Get the import path of the package.
pkgImportPath := rootImportPath
if root != path {
pkgImportPath = rootImportPath + "/" + filepath.ToSlash(path[len(root)+1:])
}
// Parse files within the path.
var pkgs map[string]*ast.Package
fset := token.NewFileSet()
pkgs, err = parser.ParseDir(fset, path, func(f os.FileInfo) bool {
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
}, 0)
if err != nil {
if errList, ok := err.(scanner.ErrorList); ok {
var pos = errList[0].Pos
compileError = &revel.Error{
SourceType: ".go source",
Title: "Go Compilation Error",
Path: pos.Filename,
Description: errList[0].Msg,
Line: pos.Line,
Column: pos.Column,
SourceLines: revel.MustReadLines(pos.Filename),
}
errorLink := revel.Config.StringDefault("error.link", "")
if errorLink != "" {
compileError.SetLink(errorLink)
}
return compileError
}
// This is exception, err alredy checked above. Here just a print
ast.Print(nil, err)
log.Fatalf("Failed to parse dir: %s", err)
}
// Skip "main" packages.
delete(pkgs, "main")
// If there is no code in this directory, skip it.
if len(pkgs) == 0 {
return nil
}
// There should be only one package in this directory.
if len(pkgs) > 1 {
log.Println("Most unexpected! Multiple packages in a single directory:", pkgs)
}
var pkg *ast.Package
for _, v := range pkgs {
pkg = v
}
srcInfo = appendSourceInfo(srcInfo, processPackage(fset, pkgImportPath, path, pkg))
return nil
})
}
return srcInfo, compileError
}
func appendSourceInfo(srcInfo1, srcInfo2 *SourceInfo) *SourceInfo {
if srcInfo1 == nil {
return srcInfo2
}
srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...)
srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...)
for k, v := range srcInfo2.ValidationKeys {
if _, ok := srcInfo1.ValidationKeys[k]; ok {
log.Println("Key conflict when scanning validation calls:", k)
continue
}
srcInfo1.ValidationKeys[k] = v
}
return srcInfo1
}
func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *SourceInfo {
var (
structSpecs []*TypeInfo
initImportPaths []string
methodSpecs = make(methodMap)
validationKeys = make(map[string]map[int]string)
scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") ||
strings.Contains(pkgImportPath, "/controllers/")
scanTests = strings.HasSuffix(pkgImportPath, "/tests") ||
strings.Contains(pkgImportPath, "/tests/")
)
// For each source file in the package...
for _, file := range pkg.Files {
// Imports maps the package key to the full import path.
// e.g. import "sample/app/models" => "models": "sample/app/models"
imports := map[string]string{}
// For each declaration in the source file...
for _, decl := range file.Decls {
addImports(imports, decl, pkgPath)
if scanControllers {
// Match and add both structs and methods
structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset)
appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports)
} else if scanTests {
structSpecs = appendStruct(structSpecs, pkgImportPath, pkg, decl, imports, fset)
}
// If this is a func...
if funcDecl, ok := decl.(*ast.FuncDecl); ok {
// Scan it for validation calls
lineKeys := getValidationKeys(fset, funcDecl, imports)
if len(lineKeys) > 0 {
validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys
}
// Check if it's an init function.
if funcDecl.Name.Name == "init" {
initImportPaths = []string{pkgImportPath}
}
}
}
}
// Add the method specs to the struct specs.
for _, spec := range structSpecs {
spec.MethodSpecs = methodSpecs[spec.StructName]
}
return &SourceInfo{
StructSpecs: structSpecs,
ValidationKeys: validationKeys,
InitImportPaths: initImportPaths,
}
}
// getFuncName returns a name for this func or method declaration.
// e.g. "(*Application).SayHello" for a method, "SayHello" for a func.
func getFuncName(funcDecl *ast.FuncDecl) string {
prefix := ""
if funcDecl.Recv != nil {
recvType := funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
} else {
prefix = recvType.(*ast.Ident).Name
}
prefix += "."
}
return prefix + funcDecl.Name.Name
}
func addImports(imports map[string]string, decl ast.Decl, srcDir string) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.IMPORT {
return
}
for _, spec := range genDecl.Specs {
importSpec := spec.(*ast.ImportSpec)
var pkgAlias string
if importSpec.Name != nil {
pkgAlias = importSpec.Name.Name
if pkgAlias == "_" {
continue
}
}
quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\""
fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes
// If the package was not aliased (common case), we have to import it
// to see what the package name is.
// TODO: Can improve performance here a lot:
// 1. Do not import everything over and over again. Keep a cache.
// 2. Exempt the standard library; their directories always match the package name.
// 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly
if pkgAlias == "" {
pkg, err := build.Import(fullPath, srcDir, 0)
if err != nil {
// We expect this to happen for apps using reverse routing (since we
// have not yet generated the routes). Don't log that.
if !strings.HasSuffix(fullPath, "/app/routes") {
revel.TRACE.Println("Could not find import:", fullPath)
}
continue
}
pkgAlias = pkg.Name
}
imports[pkgAlias] = fullPath
}
}
// If this Decl is a struct type definition, it is summarized and added to specs.
// Else, specs is returned unchanged.
func appendStruct(specs []*TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*TypeInfo {
// Filter out non-Struct type declarations.
spec, found := getStructTypeDecl(decl, fset)
if !found {
return specs
}
structType := spec.Type.(*ast.StructType)
// At this point we know it's a type declaration for a struct.
// Fill in the rest of the info by diving into the fields.
// Add it provisionally to the Controller list -- it's later filtered using field info.
controllerSpec := &TypeInfo{
StructName: spec.Name.Name,
ImportPath: pkgImportPath,
PackageName: pkg.Name,
}
for _, field := range structType.Fields.List {
// If field.Names is set, it's not an embedded type.
if field.Names != nil {
continue
}
// A direct "sub-type" has an ast.Field as either:
// Ident { "AppController" }
// SelectorExpr { "rev", "Controller" }
// Additionally, that can be wrapped by StarExprs.
fieldType := field.Type
pkgName, typeName := func() (string, string) {
// Drill through any StarExprs.
for {
if starExpr, ok := fieldType.(*ast.StarExpr); ok {
fieldType = starExpr.X
continue
}
break
}
// If the embedded type is in the same package, it's an Ident.
if ident, ok := fieldType.(*ast.Ident); ok {
return "", ident.Name
}
if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok {
if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok {
return pkgIdent.Name, selectorExpr.Sel.Name
}
}
return "", ""
}()
// If a typename wasn't found, skip it.
if typeName == "" {
continue
}
// Find the import path for this type.
// If it was referenced without a package name, use the current package import path.
// Else, look up the package's import path by name.
var importPath string
if pkgName == "" {
importPath = pkgImportPath
} else {
var ok bool
if importPath, ok = imports[pkgName]; !ok {
log.Print("Failed to find import path for ", pkgName, ".", typeName)
continue
}
}
controllerSpec.embeddedTypes = append(controllerSpec.embeddedTypes, &embeddedTypeName{
ImportPath: importPath,
StructName: typeName,
})
}
return append(specs, controllerSpec)
}
// If decl is a Method declaration, it is summarized and added to the array
// underneath its receiver type.
// e.g. "Login" => {MethodSpec, MethodSpec, ..}
func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) {
// Func declaration?
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok {
return
}
// Have a receiver?
if funcDecl.Recv == nil {
return
}
// Is it public?
if !funcDecl.Name.IsExported() {
return
}
// Does it return a Result?
if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 {
return
}
selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr)
if !ok {
return
}
if selExpr.Sel.Name != "Result" {
return
}
if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != revel.RevelImportPath {
return
}
method := &MethodSpec{
Name: funcDecl.Name.Name,
}
// Add a description of the arguments to the method.
for _, field := range funcDecl.Type.Params.List {
for _, name := range field.Names {
var importPath string
typeExpr := NewTypeExpr(pkgName, field.Type)
if !typeExpr.Valid {
log.Printf("Didn't understand argument '%s' of action %s. Ignoring.\n", name, getFuncName(funcDecl))
return // We didn't understand one of the args. Ignore this action.
}
if typeExpr.PkgName != "" {
var ok bool
if importPath, ok = imports[typeExpr.PkgName]; !ok {
log.Println("Failed to find import for arg of type:", typeExpr.TypeName(""))
}
}
method.Args = append(method.Args, &MethodArg{
Name: name.Name,
TypeExpr: typeExpr,
ImportPath: importPath,
})
}
}
// Add a description of the calls to Render from the method.
// Inspect every node (e.g. always return true).
method.RenderCalls = []*methodCall{}
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// Is it a function call?
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// Is it calling (*Controller).Render?
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// The type of the receiver is not easily available, so just store every
// call to any method called Render.
if selExpr.Sel.Name != "Render" {
return true
}
// Add this call's args to the renderArgs.
pos := fset.Position(callExpr.Rparen)
methodCall := &methodCall{
Line: pos.Line,
Names: []string{},
}
for _, arg := range callExpr.Args {
argIdent, ok := arg.(*ast.Ident)
if !ok {
continue
}
methodCall.Names = append(methodCall.Names, argIdent.Name)
}
method.RenderCalls = append(method.RenderCalls, methodCall)
return true
})
var recvTypeName string
var recvType = funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
recvTypeName = recvStarType.X.(*ast.Ident).Name
} else {
recvTypeName = recvType.(*ast.Ident).Name
}
mm[recvTypeName] = append(mm[recvTypeName], method)
}
// Scan app source code for calls to X.Y(), where X is of type *Validation.
//
// Recognize these scenarios:
// - "Y" = "Validation" and is a member of the receiver.
// (The common case for inline validation)
// - "X" is passed in to the func as a parameter.
// (For structs implementing Validated)
//
// The line number to which a validation call is attributed is that of the
// surrounding ExprStmt. This is so that it matches what runtime.Callers()
// reports.
//
// The end result is that we can set the default validation key for each call to
// be the same as the local variable.
func getValidationKeys(fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string {
var (
lineKeys = make(map[int]string)
// Check the func parameters and the receiver's members for the *revel.Validation type.
validationParam = getValidationParameter(funcDecl, imports)
)
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// e.g. c.Validation.Required(arg) or v.Required(arg)
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// e.g. c.Validation.Required or v.Required
funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
switch x := funcSelector.X.(type) {
case *ast.SelectorExpr: // e.g. c.Validation
if x.Sel.Name != "Validation" {
return true
}
case *ast.Ident: // e.g. v
if validationParam == nil || x.Obj != validationParam {
return true
}
default:
return true
}
if len(callExpr.Args) == 0 {
return true
}
// Given the validation expression, extract the key.
key := callExpr.Args[0]
switch expr := key.(type) {
case *ast.BinaryExpr:
// If the argument is a binary expression, take the first expression.
// (e.g. c.Validation.Required(myName != ""))
key = expr.X
case *ast.UnaryExpr:
// If the argument is a unary expression, drill in.
// (e.g. c.Validation.Required(!myBool)
key = expr.X
case *ast.BasicLit:
// If it's a literal, skip it.
return true
}
if typeExpr := NewTypeExpr("", key); typeExpr.Valid {
lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("")
}
return true
})
return lineKeys
}
// Check to see if there is a *revel.Validation as an argument.
func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object {
for _, field := range funcDecl.Type.Params.List {
starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation
if !ok {
continue
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation
if !ok {
continue
}
xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev
if !ok {
continue
}
if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == revel.RevelImportPath {
return field.Names[0].Obj
}
}
return nil
}
func (s *TypeInfo) String() string {
return s.ImportPath + "." + s.StructName
}
func (s *embeddedTypeName) String() string {
return s.ImportPath + "." + s.StructName
}
// getStructTypeDecl checks if the given decl is a type declaration for a
// struct. If so, the TypeSpec is returned.
func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.TYPE {
return
}
if len(genDecl.Specs) == 0 {
revel.WARN.Printf("Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
return
}
spec = genDecl.Specs[0].(*ast.TypeSpec)
_, found = spec.Type.(*ast.StructType)
return
}
// TypesThatEmbed returns all types that (directly or indirectly) embed the
// target type, which must be a fully qualified type name,
// e.g. "github.com/revel/revel.Controller"
func (s *SourceInfo) TypesThatEmbed(targetType string) (filtered []*TypeInfo) {
// Do a search in the "embedded type graph", starting with the target type.
var (
nodeQueue = []string{targetType}
processed []string
)
for len(nodeQueue) > 0 {
controllerSimpleName := nodeQueue[0]
nodeQueue = nodeQueue[1:]
processed = append(processed, controllerSimpleName)
// Look through all known structs.
for _, spec := range s.StructSpecs {
// If this one has been processed or is already in nodeQueue, then skip it.
if revel.ContainsString(processed, spec.String()) ||
revel.ContainsString(nodeQueue, spec.String()) {
continue
}
// Look through the embedded types to see if the current type is among them.
for _, embeddedType := range spec.embeddedTypes {
// If so, add this type's simple name to the nodeQueue, and its spec to
// the filtered list.
if controllerSimpleName == embeddedType.String() {
nodeQueue = append(nodeQueue, spec.String())
filtered = append(filtered, spec)
break
}
}
}
}
return
}
// ControllerSpecs returns the all the contollers that embeds
// `revel.Controller`
func (s *SourceInfo) ControllerSpecs() []*TypeInfo {
if s.controllerSpecs == nil {
s.controllerSpecs = s.TypesThatEmbed(revel.RevelImportPath + ".Controller")
}
return s.controllerSpecs
}
// TestSuites returns the all the Application tests that embeds
// `testing.TestSuite`
func (s *SourceInfo) TestSuites() []*TypeInfo {
if s.testSuites == nil {
s.testSuites = s.TypesThatEmbed(revel.RevelImportPath + "/testing.TestSuite")
}
return s.testSuites
}
// TypeExpr provides a type name that may be rewritten to use a package name.
type TypeExpr struct {
Expr string // The unqualified type expression, e.g. "[]*MyType"
PkgName string // The default package idenifier
pkgIndex int // The index where the package identifier should be inserted.
Valid bool
}
// TypeName returns the fully-qualified type name for this expression.
// The caller may optionally specify a package name to override the default.
func (e TypeExpr) TypeName(pkgOverride string) string {
pkgName := revel.FirstNonEmpty(pkgOverride, e.PkgName)
if pkgName == "" {
return e.Expr
}
return e.Expr[:e.pkgIndex] + pkgName + "." + e.Expr[e.pkgIndex:]
}
// NewTypeExpr returns the syntactic expression for referencing this type in Go.
func NewTypeExpr(pkgName string, expr ast.Expr) TypeExpr {
switch t := expr.(type) {
case *ast.Ident:
if IsBuiltinType(t.Name) {
pkgName = ""
}
return TypeExpr{t.Name, pkgName, 0, true}
case *ast.SelectorExpr:
e := NewTypeExpr(pkgName, t.X)
return TypeExpr{t.Sel.Name, e.Expr, 0, e.Valid}
case *ast.StarExpr:
e := NewTypeExpr(pkgName, t.X)
return TypeExpr{"*" + e.Expr, e.PkgName, e.pkgIndex + 1, e.Valid}
case *ast.ArrayType:
e := NewTypeExpr(pkgName, t.Elt)
return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid}
case *ast.Ellipsis:
e := NewTypeExpr(pkgName, t.Elt)
return TypeExpr{"[]" + e.Expr, e.PkgName, e.pkgIndex + 2, e.Valid}
default:
log.Println("Failed to generate name for field. Make sure the field name is valid.")
}
return TypeExpr{Valid: false}
}
var builtInTypes = map[string]struct{}{
"bool": {},
"byte": {},
"complex128": {},
"complex64": {},
"error": {},
"float32": {},
"float64": {},
"int": {},
"int16": {},
"int32": {},
"int64": {},
"int8": {},
"rune": {},
"string": {},
"uint": {},
"uint16": {},
"uint32": {},
"uint64": {},
"uint8": {},
"uintptr": {},
}
// IsBuiltinType checks the given type is built-in types of Go
func IsBuiltinType(name string) bool {
_, ok := builtInTypes[name]
return ok
}
func importPathFromPath(root string) string {
vendoringPath := revel.BasePath + "/vendor/"
if strings.HasPrefix(root, vendoringPath) {
return filepath.ToSlash(root[len(vendoringPath):])
}
for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
srcPath := filepath.Join(gopath, "src")
if strings.HasPrefix(root, srcPath) {
return filepath.ToSlash(root[len(srcPath)+1:])
}
}
srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg")
if strings.HasPrefix(root, srcPath) {
revel.WARN.Println("Code path should be in GOPATH, but is in GOROOT:", root)
return filepath.ToSlash(root[len(srcPath)+1:])
}
revel.ERROR.Println("Unexpected! Code path is not in GOPATH:", root)
return ""
}

View File

@@ -0,0 +1,174 @@
package logger
import (
"github.com/mattn/go-colorable"
"gopkg.in/natefinch/lumberjack.v2"
"io"
"os"
)
type CompositeMultiHandler struct {
DebugHandler LogHandler
InfoHandler LogHandler
WarnHandler LogHandler
ErrorHandler LogHandler
CriticalHandler LogHandler
}
func NewCompositeMultiHandler() (*CompositeMultiHandler, LogHandler) {
cw := &CompositeMultiHandler{}
return cw, cw
}
func (h *CompositeMultiHandler) Log(r *Record) (err error) {
var handler LogHandler
switch r.Level {
case LvlInfo:
handler = h.InfoHandler
case LvlDebug:
handler = h.DebugHandler
case LvlWarn:
handler = h.WarnHandler
case LvlError:
handler = h.ErrorHandler
case LvlCrit:
handler = h.CriticalHandler
}
// Embed the caller function in the context
if handler != nil {
handler.Log(r)
}
return
}
func (h *CompositeMultiHandler) SetHandler(handler LogHandler, replace bool, level LogLevel) {
if handler == nil {
// Ignore empty handler
return
}
source := &h.DebugHandler
switch level {
case LvlDebug:
source = &h.DebugHandler
case LvlInfo:
source = &h.InfoHandler
case LvlWarn:
source = &h.WarnHandler
case LvlError:
source = &h.ErrorHandler
case LvlCrit:
source = &h.CriticalHandler
}
if !replace && *source != nil {
// If we are not replacing the source make sure that the level handler is applied first
if _, isLevel := (*source).(*LevelFilterHandler); !isLevel {
*source = LevelHandler(level, *source)
}
// If this already was a list add a new logger to it
if ll, found := (*source).(*ListLogHandler); found {
ll.Add(handler)
} else {
*source = NewListLogHandler(*source, handler)
}
} else {
*source = handler
}
}
// For the multi handler set the handler, using the LogOptions defined
func (h *CompositeMultiHandler) SetHandlers(handler LogHandler, options *LogOptions) {
if len(options.Levels) == 0 {
options.Levels = LvlAllList
}
// Set all levels
for _, lvl := range options.Levels {
h.SetHandler(handler, options.ReplaceExistingHandler, lvl)
}
}
func (h *CompositeMultiHandler) SetJson(writer io.Writer, options *LogOptions) {
handler := CallerFileHandler(StreamHandler(writer, JsonFormatEx(
options.GetBoolDefault("pretty", false),
options.GetBoolDefault("lineSeparated", true),
)))
if options.HandlerWrap != nil {
handler = options.HandlerWrap.SetChild(handler)
}
h.SetHandlers(handler, options)
}
// Use built in rolling function
func (h *CompositeMultiHandler) SetJsonFile(filePath string, options *LogOptions) {
writer := &lumberjack.Logger{
Filename: filePath,
MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes
MaxAge: options.GetIntDefault("maxAgeDays", 7), //days
MaxBackups: options.GetIntDefault("maxBackups", 7),
Compress: options.GetBoolDefault("compress", true),
}
h.SetJson(writer, options)
}
func (h *CompositeMultiHandler) SetTerminal(writer io.Writer, options *LogOptions) {
streamHandler := StreamHandler(
writer,
TerminalFormatHandler(
options.GetBoolDefault("noColor", false),
options.GetBoolDefault("smallDate", true)))
if os.Stdout == writer {
streamHandler = StreamHandler(
colorable.NewColorableStdout(),
TerminalFormatHandler(
options.GetBoolDefault("noColor", false),
options.GetBoolDefault("smallDate", true)))
} else if os.Stderr == writer {
streamHandler = StreamHandler(
colorable.NewColorableStderr(),
TerminalFormatHandler(
options.GetBoolDefault("noColor", false),
options.GetBoolDefault("smallDate", true)))
}
handler := CallerFileHandler(streamHandler)
if options.HandlerWrap != nil {
handler = options.HandlerWrap.SetChild(handler)
}
h.SetHandlers(handler, options)
}
// Use built in rolling function
func (h *CompositeMultiHandler) SetTerminalFile(filePath string, options *LogOptions) {
writer := &lumberjack.Logger{
Filename: filePath,
MaxSize: options.GetIntDefault("maxSizeMB", 1024), // megabytes
MaxAge: options.GetIntDefault("maxAgeDays", 7), //days
MaxBackups: options.GetIntDefault("maxBackups", 7),
Compress: options.GetBoolDefault("compress", true),
}
h.SetTerminal(writer, options)
}
func (h *CompositeMultiHandler) Disable(levels ...LogLevel) {
if len(levels) == 0 {
levels = LvlAllList
}
for _, level := range levels {
switch level {
case LvlDebug:
h.DebugHandler = nil
case LvlInfo:
h.InfoHandler = nil
case LvlWarn:
h.WarnHandler = nil
case LvlError:
h.ErrorHandler = nil
case LvlCrit:
h.CriticalHandler = nil
}
}
}

15
logger/doc.go Normal file
View File

@@ -0,0 +1,15 @@
/*
Package logger contains filters and handles for the logging utilities in Revel.
These facilities all currently use the logging library called log15 at
https://github.com/inconshreveable/log15
Defining handlers happens as follows
1) ALL handlers (log.all.output) replace any existing handlers
2) Output handlers (log.error.output) replace any existing handlers
3) Filter handlers (log.xxx.filter, log.xxx.nfilter) append to existing handlers,
note log.all.filter is treated as a filter handler, so it will NOT replace existing ones
*/
package logger

210
logger/handlers.go Normal file
View File

@@ -0,0 +1,210 @@
package logger
import (
"fmt"
"io"
)
type LevelFilterHandler struct {
Level LogLevel
h LogHandler
}
// Filters out records which do not match the level
// Uses the `log15.FilterHandler` to perform this task
func LevelHandler(lvl LogLevel, h LogHandler) LogHandler {
return &LevelFilterHandler{lvl, h}
}
// The implementation of the Log
func (h LevelFilterHandler) Log(r *Record) error {
if r.Level == h.Level {
return h.h.Log(r)
}
return nil
}
// Filters out records which do not match the level
// Uses the `log15.FilterHandler` to perform this task
func MinLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
return FilterHandler(func(r *Record) (pass bool) {
return r.Level <= lvl
}, h)
}
// Filters out records which match the level
// Uses the `log15.FilterHandler` to perform this task
func NotLevelHandler(lvl LogLevel, h LogHandler) LogHandler {
return FilterHandler(func(r *Record) (pass bool) {
return r.Level != lvl
}, h)
}
func CallerFileHandler(h LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
r.Context.Add("caller", fmt.Sprint(r.Call))
return h.Log(r)
})
}
// Adds in a context called `caller` to the record (contains file name and line number like `foo.go:12`)
// Uses the `log15.CallerFuncHandler` to perform this task
func CallerFuncHandler(h LogHandler) LogHandler {
return CallerFuncHandler(h)
}
// Filters out records which match the key value pair
// Uses the `log15.MatchFilterHandler` to perform this task
func MatchHandler(key string, value interface{}, h LogHandler) LogHandler {
return MatchFilterHandler(key, value, h)
}
// MatchFilterHandler returns a Handler that only writes records
// to the wrapped Handler if the given key in the logged
// context matches the value. For example, to only log records
// from your ui package:
//
// log.MatchFilterHandler("pkg", "app/ui", log.StdoutHandler)
//
func MatchFilterHandler(key string, value interface{}, h LogHandler) LogHandler {
return FilterHandler(func(r *Record) (pass bool) {
return r.Context[key] == value
}, h)
}
// If match then A handler is called otherwise B handler is called
func MatchAbHandler(key string, value interface{}, a, b LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
if r.Context[key] == value {
return a.Log(r)
} else if b != nil {
return b.Log(r)
}
return nil
})
}
// The nil handler is used if logging for a specific request needs to be turned off
func NilHandler() LogHandler {
return FuncHandler(func(r *Record) error {
return nil
})
}
// Match all values in map to log
func MatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
return matchMapHandler(matchMap, false, a)
}
// Match !(Match all values in map to log) The inverse of MatchMapHandler
func NotMatchMapHandler(matchMap map[string]interface{}, a LogHandler) LogHandler {
return matchMapHandler(matchMap, true, a)
}
// Rather then chaining multiple filter handlers, process all here
func matchMapHandler(matchMap map[string]interface{}, inverse bool, a LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
matchCount := 0
for k, v := range matchMap {
value, found := r.Context[k]
if !found {
return nil
}
// Test for two failure cases
if value == v && inverse || value != v && !inverse {
return nil
} else {
matchCount++
}
}
if matchCount != len(matchMap) {
return nil
}
return a.Log(r)
})
}
// Filters out records which do not match the key value pair
// Uses the `log15.FilterHandler` to perform this task
func NotMatchHandler(key string, value interface{}, h LogHandler) LogHandler {
return FilterHandler(func(r *Record) (pass bool) {
return r.Context[key] != value
}, h)
}
func MultiHandler(hs ...LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
for _, h := range hs {
// what to do about failures?
h.Log(r)
}
return nil
})
}
// StreamHandler writes log records to an io.Writer
// with the given format. StreamHandler can be used
// to easily begin writing log records to other
// outputs.
//
// StreamHandler wraps itself with LazyHandler and SyncHandler
// to evaluate Lazy objects and perform safe concurrent writes.
func StreamHandler(wr io.Writer, fmtr LogFormat) LogHandler {
h := FuncHandler(func(r *Record) error {
_, err := wr.Write(fmtr.Format(r))
return err
})
return LazyHandler(SyncHandler(h))
}
// Filter handler
func FilterHandler(fn func(r *Record) bool, h LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
if fn(r) {
return h.Log(r)
}
return nil
})
}
// List log handler handles a list of LogHandlers
type ListLogHandler struct {
handlers []LogHandler
}
// Create a new list of log handlers
func NewListLogHandler(h1, h2 LogHandler) *ListLogHandler {
ll := &ListLogHandler{handlers: []LogHandler{h1, h2}}
return ll
}
// Log the record
func (ll *ListLogHandler) Log(r *Record) (err error) {
for _, handler := range ll.handlers {
if err == nil {
err = handler.Log(r)
} else {
handler.Log(r)
}
}
return
}
// Add another log handler
func (ll *ListLogHandler) Add(h LogHandler) {
if h != nil {
ll.handlers = append(ll.handlers, h)
}
}
// Remove a log handler
func (ll *ListLogHandler) Del(h LogHandler) {
if h != nil {
for i, handler := range ll.handlers {
if handler == h {
ll.handlers = append(ll.handlers[:i], ll.handlers[i+1:]...)
}
}
}
}

189
logger/init.go Normal file
View File

@@ -0,0 +1,189 @@
package logger
// Get all handlers based on the Config (if available)
import (
"fmt"
"github.com/revel/config"
"log"
"os"
"path/filepath"
"strings"
)
func InitializeFromConfig(basePath string, config *config.Context) (c *CompositeMultiHandler) {
// If running in test mode suppress anything that is not an error
if config != nil && config.BoolDefault(TEST_MODE_FLAG, false) {
// Preconfigure all the options
config.SetOption("log.info.output", "none")
config.SetOption("log.debug.output", "none")
config.SetOption("log.warn.output", "none")
config.SetOption("log.error.output", "stderr")
config.SetOption("log.crit.output", "stderr")
}
// If the configuration has an all option we can skip some
c, _ = NewCompositeMultiHandler()
// Filters are assigned first, non filtered items override filters
if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
initAllLog(c, basePath, config)
}
initLogLevels(c, basePath, config)
if c.CriticalHandler == nil && c.ErrorHandler != nil {
c.CriticalHandler = c.ErrorHandler
}
if config != nil && !config.BoolDefault(TEST_MODE_FLAG, false) {
initFilterLog(c, basePath, config)
if c.CriticalHandler == nil && c.ErrorHandler != nil {
c.CriticalHandler = c.ErrorHandler
}
initRequestLog(c, basePath, config)
}
return c
}
// Init the log.all configuration options
func initAllLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
if config != nil {
extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
if output, found := config.String("log.all.output"); found {
// Set all output for the specified handler
if extraLogFlag {
log.Printf("Adding standard handler for levels to >%s< ", output)
}
initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, LvlAllList...))
}
}
}
// Init the filter options
// log.all.filter ....
// log.error.filter ....
func initFilterLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
if config != nil {
extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
for _, logFilter := range logFilterList {
// Init for all filters
for _, name := range []string{"all", "debug", "info", "warn", "error", "crit",
"trace", // TODO trace is deprecated
} {
optionList := config.Options(logFilter.LogPrefix + name + logFilter.LogSuffix)
for _, option := range optionList {
splitOptions := strings.Split(option, ".")
keyMap := map[string]interface{}{}
for x := 3; x < len(splitOptions); x += 2 {
keyMap[splitOptions[x]] = splitOptions[x+1]
}
phandler := logFilter.parentHandler(keyMap)
if extraLogFlag {
log.Printf("Adding key map handler %s %s output %s", option, name, config.StringDefault(option, ""))
fmt.Printf("Adding key map handler %s %s output %s matching %#v\n", option, name, config.StringDefault(option, ""), keyMap)
}
if name == "all" {
initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler))
} else {
initHandlerFor(c, config.StringDefault(option, ""), basePath, NewLogOptions(config, false, phandler, toLevel[name]))
}
}
}
}
}
}
// Init the log.error, log.warn etc configuration options
func initLogLevels(c *CompositeMultiHandler, basePath string, config *config.Context) {
for _, name := range []string{"debug", "info", "warn", "error", "crit",
"trace", // TODO trace is deprecated
} {
if config != nil {
extraLogFlag := config.BoolDefault(SPECIAL_USE_FLAG, false)
output, found := config.String("log." + name + ".output")
if found {
if extraLogFlag {
log.Printf("Adding standard handler %s output %s", name, output)
}
initHandlerFor(c, output, basePath, NewLogOptions(config, true, nil, toLevel[name]))
}
// Gets the list of options with said prefix
} else {
initHandlerFor(c, "stderr", basePath, NewLogOptions(config, true, nil, toLevel[name]))
}
}
}
// Init the request log options
func initRequestLog(c *CompositeMultiHandler, basePath string, config *config.Context) {
// Request logging to a separate output handler
// This takes the InfoHandlers and adds a MatchAbHandler handler to it to direct
// context with the word "section=requestlog" to that handler.
// Note if request logging is not enabled the MatchAbHandler will not be added and the
// request log messages will be sent out the INFO handler
outputRequest := "stdout"
if config != nil {
outputRequest = config.StringDefault("log.request.output", "")
}
oldInfo := c.InfoHandler
c.InfoHandler = nil
if outputRequest != "" {
initHandlerFor(c, outputRequest, basePath, NewLogOptions(config, false, nil, LvlInfo))
}
if c.InfoHandler != nil || oldInfo != nil {
if c.InfoHandler == nil {
c.InfoHandler = oldInfo
} else {
c.InfoHandler = MatchAbHandler("section", "requestlog", c.InfoHandler, oldInfo)
}
}
}
// Returns a handler for the level using the output string
// Accept formats for output string are
// LogFunctionMap[value] callback function
// `stdout` `stderr` `full/file/path/to/location/app.log` `full/file/path/to/location/app.json`
func initHandlerFor(c *CompositeMultiHandler, output, basePath string, options *LogOptions) {
if options.Ctx != nil {
options.SetExtendedOptions(
"noColor", !options.Ctx.BoolDefault("log.colorize", true),
"smallDate", options.Ctx.BoolDefault("log.smallDate", true),
"maxSize", options.Ctx.IntDefault("log.maxsize", 1024*10),
"maxAge", options.Ctx.IntDefault("log.maxage", 14),
"maxBackups", options.Ctx.IntDefault("log.maxbackups", 14),
"compressBackups", !options.Ctx.BoolDefault("log.compressBackups", true),
)
}
output = strings.TrimSpace(output)
if funcHandler, found := LogFunctionMap[output]; found {
funcHandler(c, options)
} else {
switch output {
case "":
fallthrough
case "off":
// No handler, discard data
default:
// Write to file specified
if !filepath.IsAbs(output) {
output = filepath.Join(basePath, output)
}
if err := os.MkdirAll(filepath.Dir(output), 0755); err != nil {
log.Panic(err)
}
if strings.HasSuffix(output, "json") {
c.SetJsonFile(output, options)
} else {
// Override defaults for a terminal file
options.SetExtendedOptions("noColor", true)
options.SetExtendedOptions("smallDate", false)
c.SetTerminalFile(output, options)
}
}
}
return
}

273
logger/init_test.go Normal file
View File

@@ -0,0 +1,273 @@
// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package logger_test
import (
"github.com/revel/config"
"github.com/revel/revel/logger"
"github.com/stretchr/testify/assert"
"os"
"strings"
"testing"
)
type (
// A counter for the tester
testCounter struct {
debug, info, warn, error, critical int
}
// The data to tes
testData struct {
config []string
result testResult
tc *testCounter
}
// The test result
testResult struct {
debug, info, warn, error, critical int
}
)
// Single test cases
var singleCases = []testData{
{config: []string{"log.crit.output"},
result: testResult{0, 0, 0, 0, 1}},
{config: []string{"log.error.output"},
result: testResult{0, 0, 0, 1, 1}},
{config: []string{"log.warn.output"},
result: testResult{0, 0, 1, 0, 0}},
{config: []string{"log.info.output"},
result: testResult{0, 1, 0, 0, 0}},
{config: []string{"log.debug.output"},
result: testResult{1, 0, 0, 0, 0}},
}
// Test singles
func TestSingleCases(t *testing.T) {
rootLog := logger.New()
for _, testCase := range singleCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// Filter test cases
var filterCases = []testData{
{config: []string{"log.crit.filter.module.app"},
result: testResult{0, 0, 0, 0, 1}},
{config: []string{"log.crit.filter.module.appa"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.error.filter.module.app"},
result: testResult{0, 0, 0, 1, 1}},
{config: []string{"log.error.filter.module.appa"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.warn.filter.module.app"},
result: testResult{0, 0, 1, 0, 0}},
{config: []string{"log.warn.filter.module.appa"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.info.filter.module.app"},
result: testResult{0, 1, 0, 0, 0}},
{config: []string{"log.info.filter.module.appa"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.debug.filter.module.app"},
result: testResult{1, 0, 0, 0, 0}},
{config: []string{"log.debug.filter.module.appa"},
result: testResult{0, 0, 0, 0, 0}},
}
// Filter test
func TestFilterCases(t *testing.T) {
rootLog := logger.New("module", "app")
for _, testCase := range filterCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// Inverse test cases
var nfilterCases = []testData{
{config: []string{"log.crit.nfilter.module.appa"},
result: testResult{0, 0, 0, 0, 1}},
{config: []string{"log.crit.nfilter.modules.appa"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.crit.nfilter.module.app"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.error.nfilter.module.appa"}, // Special case, when error is not nill critical inherits from error
result: testResult{0, 0, 0, 1, 1}},
{config: []string{"log.error.nfilter.module.app"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.warn.nfilter.module.appa"},
result: testResult{0, 0, 1, 0, 0}},
{config: []string{"log.warn.nfilter.module.app"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.info.nfilter.module.appa"},
result: testResult{0, 1, 0, 0, 0}},
{config: []string{"log.info.nfilter.module.app"},
result: testResult{0, 0, 0, 0, 0}},
{config: []string{"log.debug.nfilter.module.appa"},
result: testResult{1, 0, 0, 0, 0}},
{config: []string{"log.debug.nfilter.module.app"},
result: testResult{0, 0, 0, 0, 0}},
}
// Inverse test
func TestNotFilterCases(t *testing.T) {
rootLog := logger.New("module", "app")
for _, testCase := range nfilterCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// off test cases
var offCases = []testData{
{config: []string{"log.all.output", "log.error.output=off"},
result: testResult{1, 1, 1, 0, 1}},
}
// Off test
func TestOffCases(t *testing.T) {
rootLog := logger.New("module", "app")
for _, testCase := range offCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// Duplicate test cases
var duplicateCases = []testData{
{config: []string{"log.all.output", "log.error.output", "log.error.filter.module.app"},
result: testResult{1, 1, 1, 2, 1}},
}
// test duplicate cases
func TestDuplicateCases(t *testing.T) {
rootLog := logger.New("module", "app")
for _, testCase := range duplicateCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// Contradicting cases
var contradictCases = []testData{
{config: []string{"log.all.output", "log.error.output=off", "log.all.output"},
result: testResult{1, 1, 1, 0, 1}},
{config: []string{"log.all.output", "log.error.output=off", "log.debug.filter.module.app"},
result: testResult{2, 1, 1, 0, 1}},
{config: []string{"log.all.filter.module.app", "log.info.output=off", "log.info.filter.module.app"},
result: testResult{1, 2, 1, 1, 1}},
{config: []string{"log.all.output", "log.info.output=off", "log.info.filter.module.app"},
result: testResult{1, 1, 1, 1, 1}},
}
// Contradiction test
func TestContradictCases(t *testing.T) {
rootLog := logger.New("module", "app")
for _, testCase := range contradictCases {
testCase.logTest(rootLog, t)
testCase.validate(t)
}
}
// All test cases
var allCases = []testData{
{config: []string{"log.all.filter.module.app"},
result: testResult{1, 1, 1, 1, 1}},
{config: []string{"log.all.output"},
result: testResult{2, 2, 2, 2, 2}},
}
// All tests
func TestAllCases(t *testing.T) {
rootLog := logger.New("module", "app")
for i, testCase := range allCases {
testCase.logTest(rootLog, t)
allCases[i] = testCase
}
rootLog = logger.New()
for i, testCase := range allCases {
testCase.logTest(rootLog, t)
allCases[i] = testCase
}
for _, testCase := range allCases {
testCase.validate(t)
}
}
func (c *testCounter) Log(r *logger.Record) error {
switch r.Level {
case logger.LvlDebug:
c.debug++
case logger.LvlInfo:
c.info++
case logger.LvlWarn:
c.warn++
case logger.LvlError:
c.error++
case logger.LvlCrit:
c.critical++
default:
panic("Unknown log level")
}
return nil
}
func (td *testData) logTest(rootLog logger.MultiLogger, t *testing.T) {
if td.tc == nil {
td.tc = &testCounter{}
counterInit(td.tc)
}
newContext := config.NewContext()
for _, i := range td.config {
iout := strings.Split(i, "=")
if len(iout) > 1 {
newContext.SetOption(iout[0], iout[1])
} else {
newContext.SetOption(i, "test")
}
}
newContext.SetOption("specialUseFlag", "true")
handler := logger.InitializeFromConfig("test", newContext)
rootLog.SetHandler(handler)
td.runLogTest(rootLog)
}
func (td *testData) runLogTest(log logger.MultiLogger) {
log.Debug("test")
log.Info("test")
log.Warn("test")
log.Error("test")
log.Crit("test")
}
func (td *testData) validate(t *testing.T) {
t.Logf("Test %#v expected %#v", td.tc, td.result)
assert.Equal(t, td.result.debug, td.tc.debug, "Debug failed "+strings.Join(td.config, " "))
assert.Equal(t, td.result.info, td.tc.info, "Info failed "+strings.Join(td.config, " "))
assert.Equal(t, td.result.warn, td.tc.warn, "Warn failed "+strings.Join(td.config, " "))
assert.Equal(t, td.result.error, td.tc.error, "Error failed "+strings.Join(td.config, " "))
assert.Equal(t, td.result.critical, td.tc.critical, "Critical failed "+strings.Join(td.config, " "))
}
// Add test to the function map
func counterInit(tc *testCounter) {
logger.LogFunctionMap["test"] = func(c *logger.CompositeMultiHandler, logOptions *logger.LogOptions) {
// Output to the test log and the stdout
outHandler := logger.LogHandler(
logger.NewListLogHandler(tc,
logger.StreamHandler(os.Stdout, logger.TerminalFormatHandler(false, true))),
)
if logOptions.HandlerWrap != nil {
outHandler = logOptions.HandlerWrap.SetChild(outHandler)
}
c.SetHandlers(outHandler, logOptions)
}
}

View File

@@ -0,0 +1,37 @@
package logger
import (
"os"
)
// The log function map can be added to, so that you can specify your own logging mechanism
// it has defaults for off, stdout, stderr
var LogFunctionMap = map[string]func(*CompositeMultiHandler, *LogOptions){
// Do nothing - set the logger off
"off": func(c *CompositeMultiHandler, logOptions *LogOptions) {
// Only drop the results if there is a parent handler defined
if logOptions.HandlerWrap != nil {
for _, l := range logOptions.Levels {
c.SetHandler(logOptions.HandlerWrap.SetChild(NilHandler()), logOptions.ReplaceExistingHandler, l)
}
} else {
// Clear existing handler
c.SetHandlers(NilHandler(), logOptions)
}
},
// Do nothing - set the logger off
"": func(*CompositeMultiHandler, *LogOptions) {},
// Set the levels to stdout, replace existing
"stdout": func(c *CompositeMultiHandler, logOptions *LogOptions) {
if logOptions.Ctx != nil {
logOptions.SetExtendedOptions(
"noColor", !logOptions.Ctx.BoolDefault("log.colorize", true),
"smallDate", logOptions.Ctx.BoolDefault("log.smallDate", true))
}
c.SetTerminal(os.Stdout, logOptions)
},
// Set the levels to stderr output to terminal
"stderr": func(c *CompositeMultiHandler, logOptions *LogOptions) {
c.SetTerminal(os.Stderr, logOptions)
},
}

203
logger/logger.go Normal file
View File

@@ -0,0 +1,203 @@
package logger
import (
"fmt"
"github.com/revel/config"
"time"
)
// The LogHandler defines the interface to handle the log records
type (
// The Multilogger reduces the number of exposed defined logging variables,
// and allows the output to be easily refined
MultiLogger interface {
// New returns a new Logger that has this logger's context plus the given context
New(ctx ...interface{}) MultiLogger
// SetHandler updates the logger to write records to the specified handler.
SetHandler(h LogHandler)
// Set the stack depth for the logger
SetStackDepth(int) MultiLogger
// Log a message at the given level with context key/value pairs
Debug(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters
Debugf(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs
Info(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters
Infof(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs
Warn(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters
Warnf(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs
Error(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters
Errorf(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs
Crit(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters
Critf(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs and exits
Fatal(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters and exits
Fatalf(msg string, params ...interface{})
// Log a message at the given level with context key/value pairs and panics
Panic(msg string, ctx ...interface{})
// Log a message at the given level formatting message with the parameters and panics
Panicf(msg string, params ...interface{})
}
// The log handler interface
LogHandler interface {
Log(*Record) error
//log15.Handler
}
// The log stack handler interface
LogStackHandler interface {
LogHandler
GetStack() int
}
// The log handler interface which has child logs
ParentLogHandler interface {
SetChild(handler LogHandler) LogHandler
}
// The log format interface
LogFormat interface {
Format(r *Record) []byte
}
// The log level type
LogLevel int
// Used for the callback to LogFunctionMap
LogOptions struct {
Ctx *config.Context
ReplaceExistingHandler bool
HandlerWrap ParentLogHandler
Levels []LogLevel
ExtendedOptions map[string]interface{}
}
// The log record
Record struct {
Message string // The message
Time time.Time // The time
Level LogLevel //The level
Call CallStack // The call stack if built
Context ContextMap // The context
}
// The lazy structure to implement a function to be invoked only if needed
Lazy struct {
Fn interface{} // the function
}
// Currently the only requirement for the callstack is to support the Formatter method
// which stack.Call does so we use that
CallStack interface {
fmt.Formatter // Requirement
}
)
// FormatFunc returns a new Format object which uses
// the given function to perform record formatting.
func FormatFunc(f func(*Record) []byte) LogFormat {
return formatFunc(f)
}
type formatFunc func(*Record) []byte
func (f formatFunc) Format(r *Record) []byte {
return f(r)
}
func NewRecord(message string, level LogLevel) *Record {
return &Record{Message: message, Context: ContextMap{}, Level: level}
}
const (
LvlCrit LogLevel = iota // Critical
LvlError // Error
LvlWarn // Warning
LvlInfo // Information
LvlDebug // Debug
)
// A list of all the log levels
var LvlAllList = []LogLevel{LvlDebug, LvlInfo, LvlWarn, LvlError, LvlCrit}
// Implements the ParentLogHandler
type parentLogHandler struct {
setChild func(handler LogHandler) LogHandler
}
// Create a new parent log handler
func NewParentLogHandler(callBack func(child LogHandler) LogHandler) ParentLogHandler {
return &parentLogHandler{callBack}
}
// Sets the child of the log handler
func (p *parentLogHandler) SetChild(child LogHandler) LogHandler {
return p.setChild(child)
}
// Create a new log options
func NewLogOptions(cfg *config.Context, replaceHandler bool, phandler ParentLogHandler, lvl ...LogLevel) (logOptions *LogOptions) {
logOptions = &LogOptions{
Ctx: cfg,
ReplaceExistingHandler: replaceHandler,
HandlerWrap: phandler,
Levels: lvl,
ExtendedOptions: map[string]interface{}{},
}
return
}
// Assumes options will be an even number and have a string, value syntax
func (l *LogOptions) SetExtendedOptions(options ...interface{}) {
for x := 0; x < len(options); x += 2 {
l.ExtendedOptions[options[x].(string)] = options[x+1]
}
}
// Gets a string option with default
func (l *LogOptions) GetStringDefault(option, value string) string {
if v, found := l.ExtendedOptions[option]; found {
return v.(string)
}
return value
}
// Gets an int option with default
func (l *LogOptions) GetIntDefault(option string, value int) int {
if v, found := l.ExtendedOptions[option]; found {
return v.(int)
}
return value
}
// Gets a boolean option with default
func (l *LogOptions) GetBoolDefault(option string, value bool) bool {
if v, found := l.ExtendedOptions[option]; found {
return v.(bool)
}
return value
}

142
logger/revel_logger.go Normal file
View File

@@ -0,0 +1,142 @@
package logger
import (
"fmt"
"github.com/revel/log15"
"log"
"os"
)
// This type implements the MultiLogger
type RevelLogger struct {
log15.Logger
}
// Set the systems default logger
// Default logs will be captured and handled by revel at level info
func SetDefaultLog(fromLog MultiLogger) {
log.SetOutput(loggerRewrite{Logger: fromLog, Level: log15.LvlInfo, hideDeprecated: true})
// No need to show date and time, that will be logged with revel
log.SetFlags(0)
}
func (rl *RevelLogger) Debugf(msg string, param ...interface{}) {
rl.Debug(fmt.Sprintf(msg, param...))
}
// Print a formatted info message
func (rl *RevelLogger) Infof(msg string, param ...interface{}) {
rl.Info(fmt.Sprintf(msg, param...))
}
// Print a formatted warn message
func (rl *RevelLogger) Warnf(msg string, param ...interface{}) {
rl.Warn(fmt.Sprintf(msg, param...))
}
// Print a formatted error message
func (rl *RevelLogger) Errorf(msg string, param ...interface{}) {
rl.Error(fmt.Sprintf(msg, param...))
}
// Print a formatted critical message
func (rl *RevelLogger) Critf(msg string, param ...interface{}) {
rl.Crit(fmt.Sprintf(msg, param...))
}
// Print a formatted fatal message
func (rl *RevelLogger) Fatalf(msg string, param ...interface{}) {
rl.Fatal(fmt.Sprintf(msg, param...))
}
// Print a formatted panic message
func (rl *RevelLogger) Panicf(msg string, param ...interface{}) {
rl.Panic(fmt.Sprintf(msg, param...))
}
// Print a critical message and call os.Exit(1)
func (rl *RevelLogger) Fatal(msg string, ctx ...interface{}) {
rl.Crit(msg, ctx...)
os.Exit(1)
}
// Print a critical message and panic
func (rl *RevelLogger) Panic(msg string, ctx ...interface{}) {
rl.Crit(msg, ctx...)
panic(msg)
}
// Override log15 method
func (rl *RevelLogger) New(ctx ...interface{}) MultiLogger {
old := &RevelLogger{Logger: rl.Logger.New(ctx...)}
return old
}
// Set the stack level to check for the caller
func (rl *RevelLogger) SetStackDepth(amount int) MultiLogger {
rl.Logger.SetStackDepth(amount) // Ignore the logger returned
return rl
}
// Create a new logger
func New(ctx ...interface{}) MultiLogger {
r := &RevelLogger{Logger: log15.New(ctx...)}
r.SetStackDepth(0)
return r
}
// Set the handler in the Logger
func (rl *RevelLogger) SetHandler(h LogHandler) {
rl.Logger.SetHandler(callHandler(h.Log))
}
// The function wrapper to implement the callback
type callHandler func(r *Record) error
// Log implementation, reads the record and extracts the details from the log record
// Hiding the implementation.
func (c callHandler) Log(log *log15.Record) error {
ctx := log.Ctx
var ctxMap ContextMap
if len(ctx) > 0 {
ctxMap = make(ContextMap, len(ctx) / 2)
for i := 0; i < len(ctx); i += 2 {
v := ctx[i]
key, ok := v.(string)
if !ok {
key = fmt.Sprintf("LOGGER_INVALID_KEY %v", v)
}
var value interface{}
if len(ctx) > i + 1 {
value = ctx[i + 1]
} else {
value = "LOGGER_VALUE_MISSING"
}
ctxMap[key] = value
}
} else {
ctxMap = make(ContextMap, 0)
}
r := &Record{Message: log.Msg, Context: ctxMap, Time: log.Time, Level: LogLevel(log.Lvl), Call: CallStack(log.Call)}
return c(r)
}
// Internally used contextMap, allows conversion of map to map[string]string
type ContextMap map[string]interface{}
// Convert the context map to be string only values, any non string values are ignored
func (m ContextMap) StringMap() (newMap map[string]string) {
if m != nil {
newMap = map[string]string{}
for key, value := range m {
if svalue, isstring := value.(string); isstring {
newMap[key] = svalue
}
}
}
return
}
func (m ContextMap) Add(key string, value interface{}) {
m[key] = value
}

245
logger/terminal_format.go Normal file
View File

@@ -0,0 +1,245 @@
package logger
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"sync"
"time"
)
const (
timeFormat = "2006-01-02T15:04:05-0700"
termTimeFormat = "2006/01/02 15:04:05"
termSmallTimeFormat = "15:04:05"
floatFormat = 'f'
errorKey = "REVEL_ERROR"
)
var (
levelString = map[LogLevel]string{LvlDebug: "DEBUG",
LvlInfo: "INFO", LvlWarn: "WARN", LvlError: "ERROR", LvlCrit: "CRIT"}
)
// Outputs to the terminal in a format like below
// INFO 09:11:32 server-engine.go:169: Request Stats
func TerminalFormatHandler(noColor bool, smallDate bool) LogFormat {
dateFormat := termTimeFormat
if smallDate {
dateFormat = termSmallTimeFormat
}
return FormatFunc(func(r *Record) []byte {
// Bash coloring http://misc.flogisoft.com/bash/tip_colors_and_formatting
var color = 0
switch r.Level {
case LvlCrit:
// Magenta
color = 35
case LvlError:
// Red
color = 31
case LvlWarn:
// Yellow
color = 33
case LvlInfo:
// Green
color = 32
case LvlDebug:
// Cyan
color = 36
}
b := &bytes.Buffer{}
caller, _ := r.Context["caller"].(string)
module, _ := r.Context["module"].(string)
if noColor == false && color > 0 {
if len(module) > 0 {
fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %6s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%-5s\x1b[0m %s %13s: %-40s ", color, levelString[r.Level], r.Time.Format(dateFormat), caller, r.Message)
}
} else {
fmt.Fprintf(b, "%-5s %s %6s %13s: %-40s", levelString[r.Level], r.Time.Format(dateFormat), module, caller, r.Message)
}
i := 0
for k, v := range r.Context {
if i != 0 {
b.WriteByte(' ')
}
i++
if k == "module" || k == "caller" {
continue
}
v := formatLogfmtValue(v)
// TODO: we should probably check that all of your key bytes aren't invalid
if noColor == false && color > 0 {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m=%s", color, k, v)
} else {
b.WriteString(k)
b.WriteByte('=')
b.WriteString(v)
}
}
b.WriteByte('\n')
return b.Bytes()
})
}
// formatValue formats a value for serialization
func formatLogfmtValue(value interface{}) string {
if value == nil {
return "nil"
}
if t, ok := value.(time.Time); ok {
// Performance optimization: No need for escaping since the provided
// timeFormat doesn't have any escape characters, and escaping is
// expensive.
return t.Format(termTimeFormat)
}
value = formatShared(value)
switch v := value.(type) {
case bool:
return strconv.FormatBool(v)
case float32:
return strconv.FormatFloat(float64(v), floatFormat, 3, 64)
case float64:
return strconv.FormatFloat(v, floatFormat, 7, 64)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", value)
case string:
return escapeString(v)
default:
return escapeString(fmt.Sprintf("%+v", value))
}
}
// Format the value in json format
func formatShared(value interface{}) (result interface{}) {
defer func() {
if err := recover(); err != nil {
if v := reflect.ValueOf(value); v.Kind() == reflect.Ptr && v.IsNil() {
result = "nil"
} else {
panic(err)
}
}
}()
switch v := value.(type) {
case time.Time:
return v.Format(timeFormat)
case error:
return v.Error()
case fmt.Stringer:
return v.String()
default:
return v
}
}
// A reusuable buffer for outputting data
var stringBufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
// Escape the string when needed
func escapeString(s string) string {
needsQuotes := false
needsEscape := false
for _, r := range s {
if r <= ' ' || r == '=' || r == '"' {
needsQuotes = true
}
if r == '\\' || r == '"' || r == '\n' || r == '\r' || r == '\t' {
needsEscape = true
}
}
if needsEscape == false && needsQuotes == false {
return s
}
e := stringBufPool.Get().(*bytes.Buffer)
e.WriteByte('"')
for _, r := range s {
switch r {
case '\\', '"':
e.WriteByte('\\')
e.WriteByte(byte(r))
case '\n':
e.WriteString("\\n")
case '\r':
e.WriteString("\\r")
case '\t':
e.WriteString("\\t")
default:
e.WriteRune(r)
}
}
e.WriteByte('"')
var ret string
if needsQuotes {
ret = e.String()
} else {
ret = string(e.Bytes()[1 : e.Len()-1])
}
e.Reset()
stringBufPool.Put(e)
return ret
}
// JsonFormatEx formats log records as JSON objects. If pretty is true,
// records will be pretty-printed. If lineSeparated is true, records
// will be logged with a new line between each record.
func JsonFormatEx(pretty, lineSeparated bool) LogFormat {
jsonMarshal := json.Marshal
if pretty {
jsonMarshal = func(v interface{}) ([]byte, error) {
return json.MarshalIndent(v, "", " ")
}
}
return FormatFunc(func(r *Record) []byte {
props := make(map[string]interface{})
props["t"] = r.Time
props["lvl"] = levelString[r.Level]
props["msg"] = r.Message
for k, v := range r.Context {
props[k] = formatJsonValue(v)
}
b, err := jsonMarshal(props)
if err != nil {
b, _ = jsonMarshal(map[string]string{
errorKey: err.Error(),
})
return b
}
if lineSeparated {
b = append(b, '\n')
}
return b
})
}
func formatJsonValue(value interface{}) interface{} {
value = formatShared(value)
switch value.(type) {
case int, int8, int16, int32, int64, float32, float64, uint, uint8, uint16, uint32, uint64, string:
return value
default:
return fmt.Sprintf("%+v", value)
}
}

110
logger/utils.go Normal file
View File

@@ -0,0 +1,110 @@
package logger
import (
"github.com/revel/log15"
"gopkg.in/stack.v0"
"log"
)
// Utility package to make existing logging backwards compatible
var (
// Convert the string to LogLevel
toLevel = map[string]LogLevel{"debug": LogLevel(log15.LvlDebug),
"info": LogLevel(log15.LvlInfo), "request": LogLevel(log15.LvlInfo), "warn": LogLevel(log15.LvlWarn),
"error": LogLevel(log15.LvlError), "crit": LogLevel(log15.LvlCrit),
"trace": LogLevel(log15.LvlDebug), // TODO trace is deprecated, replaced by debug
}
)
const (
// The test mode flag overrides the default log level and shows only errors
TEST_MODE_FLAG = "testModeFlag"
// The special use flag enables showing messages when the logger is setup
SPECIAL_USE_FLAG = "specialUseFlag"
)
// Returns the logger for the name
func GetLogger(name string, logger MultiLogger) (l *log.Logger) {
switch name {
case "trace": // TODO trace is deprecated, replaced by debug
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
case "debug":
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlDebug}, "", 0)
case "info":
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
case "warn":
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlWarn}, "", 0)
case "error":
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlError}, "", 0)
case "request":
l = log.New(loggerRewrite{Logger: logger, Level: log15.LvlInfo}, "", 0)
}
return l
}
// Used by the initFilterLog to handle the filters
var logFilterList = []struct {
LogPrefix, LogSuffix string
parentHandler func(map[string]interface{}) ParentLogHandler
}{{
"log.", ".filter",
func(keyMap map[string]interface{}) ParentLogHandler {
return NewParentLogHandler(func(child LogHandler) LogHandler {
return MatchMapHandler(keyMap, child)
})
},
}, {
"log.", ".nfilter",
func(keyMap map[string]interface{}) ParentLogHandler {
return NewParentLogHandler(func(child LogHandler) LogHandler {
return NotMatchMapHandler(keyMap, child)
})
},
}}
// This structure and method will handle the old output format and log it to the new format
type loggerRewrite struct {
Logger MultiLogger
Level log15.Lvl
hideDeprecated bool
}
// The message indicating that a logger is using a deprecated log mechanism
var log_deprecated = []byte("* LOG DEPRECATED * ")
// Implements the Write of the logger
func (lr loggerRewrite) Write(p []byte) (n int, err error) {
if !lr.hideDeprecated {
p = append(log_deprecated, p...)
}
n = len(p)
if len(p) > 0 && p[n-1] == '\n' {
p = p[:n-1]
n--
}
switch lr.Level {
case log15.LvlInfo:
lr.Logger.Info(string(p))
case log15.LvlDebug:
lr.Logger.Debug(string(p))
case log15.LvlWarn:
lr.Logger.Warn(string(p))
case log15.LvlError:
lr.Logger.Error(string(p))
case log15.LvlCrit:
lr.Logger.Crit(string(p))
}
return
}
// For logging purposes the call stack can be used to record the stack trace of a bad error
// simply pass it as a context field in your log statement like
// `controller.Log.Crit("This should not occur","stack",revel.NewCallStack())`
func NewCallStack() interface{} {
return stack.Trace()
}

98
logger/wrap_handlers.go Normal file
View File

@@ -0,0 +1,98 @@
package logger
// FuncHandler returns a Handler that logs records with the given
// function.
import (
"fmt"
"reflect"
"sync"
"time"
)
// Function handler wraps the declared function and returns the handler for it
func FuncHandler(fn func(r *Record) error) LogHandler {
return funcHandler(fn)
}
// The type decleration for the function
type funcHandler func(r *Record) error
// The implementation of the Log
func (h funcHandler) Log(r *Record) error {
return h(r)
}
// This function allows you to do a full declaration for the log,
// it is recommended you use FuncHandler instead
func HandlerFunc(log func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error) LogHandler {
return remoteHandler(log)
}
// The type used for the HandlerFunc
type remoteHandler func(message string, time time.Time, level LogLevel, call CallStack, context ContextMap) error
// The Log implementation
func (c remoteHandler) Log(record *Record) error {
return c(record.Message, record.Time, record.Level, record.Call, record.Context)
}
// SyncHandler can be wrapped around a handler to guarantee that
// only a single Log operation can proceed at a time. It's necessary
// for thread-safe concurrent writes.
func SyncHandler(h LogHandler) LogHandler {
var mu sync.Mutex
return FuncHandler(func(r *Record) error {
defer mu.Unlock()
mu.Lock()
return h.Log(r)
})
}
// LazyHandler writes all values to the wrapped handler after evaluating
// any lazy functions in the record's context. It is already wrapped
// around StreamHandler and SyslogHandler in this library, you'll only need
// it if you write your own Handler.
func LazyHandler(h LogHandler) LogHandler {
return FuncHandler(func(r *Record) error {
for k, v := range r.Context {
if lz, ok := v.(Lazy); ok {
value, err := evaluateLazy(lz)
if err != nil {
r.Context[errorKey] = "bad lazy " + k
} else {
v = value
}
}
}
return h.Log(r)
})
}
func evaluateLazy(lz Lazy) (interface{}, error) {
t := reflect.TypeOf(lz.Fn)
if t.Kind() != reflect.Func {
return nil, fmt.Errorf("INVALID_LAZY, not func: %+v", lz.Fn)
}
if t.NumIn() > 0 {
return nil, fmt.Errorf("INVALID_LAZY, func takes args: %+v", lz.Fn)
}
if t.NumOut() == 0 {
return nil, fmt.Errorf("INVALID_LAZY, no func return val: %+v", lz.Fn)
}
value := reflect.ValueOf(lz.Fn)
results := value.Call([]reflect.Value{})
if len(results) == 1 {
return results[0].Interface(), nil
} else {
values := make([]interface{}, len(results))
for i, v := range results {
values[i] = v.Interface()
}
return values, nil
}
}

10
model/command/build.go Normal file
View File

@@ -0,0 +1,10 @@
package command
type (
Build struct {
ImportCommand
TargetPath string `short:"t" long:"target-path" description:"Path to target folder. Folder will be completely deleted if it exists" required:"false"`
Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"`
CopySource bool `short:"s" long:"include-source" description:"Copy the source code as well"`
}
)

6
model/command/clean.go Normal file
View File

@@ -0,0 +1,6 @@
package command
type (
Clean struct {
ImportCommand
}
)

View File

@@ -0,0 +1,7 @@
package command
type (
ImportCommand struct {
ImportPath string `short:"a" long:"application-path" description:"Path to application folder" required:"false"`
}
)

14
model/command/new.go Normal file
View File

@@ -0,0 +1,14 @@
package command
type (
New struct {
ImportCommand
SkeletonPath string `short:"s" long:"skeleton" description:"Path to skeleton folder (Must exist on GO PATH)" required:"false"`
Package string `short:"p" long:"package" description:"The package name, this becomes the repfix to the app name, if defined vendored is set to true" required:"false"`
NotVendored bool `long:"no-vendor" description:"True if project should not be configured with a go.mod, this requires you to have the project on the GOPATH, this is only compatible with go versions v1.12 or older"`
Run bool `short:"r" long:"run" description:"True if you want to run the application right away"`
Callback func() error
}
)

9
model/command/package.go Normal file
View File

@@ -0,0 +1,9 @@
package command
type (
Package struct {
ImportCommand
TargetPath string `short:"t" long:"target-path" description:"Full path and filename of target package to deploy" required:"false"`
Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"`
CopySource bool `short:"s" long:"include-source" description:"Copy the source code as well"`
}
)

9
model/command/run.go Normal file
View File

@@ -0,0 +1,9 @@
package command
type (
Run struct {
ImportCommand
Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"`
Port int `short:"p" long:"port" default:"-1" description:"The port to listen" `
NoProxy bool `short:"n" long:"no-proxy" description:"True if proxy server should not be started. This will only update the main and routes files on change"`
}
)

View File

@@ -0,0 +1,9 @@
package command
type (
Test struct {
ImportCommand
Mode string `short:"m" long:"run-mode" description:"The mode to run the application in"`
Function string `short:"f" long:"suite-function" description:"The suite.function"`
}
)

6
model/command/version.go Normal file
View File

@@ -0,0 +1,6 @@
package command
type (
Version struct {
ImportCommand
}
)

353
model/command_config.go Normal file
View File

@@ -0,0 +1,353 @@
package model
import (
"fmt"
"github.com/revel/cmd"
"github.com/revel/cmd/utils"
"go/ast"
"go/build"
"go/parser"
"go/token"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/revel/cmd/model/command"
)
// The constants
const (
NEW COMMAND = iota + 1
RUN
BUILD
PACKAGE
CLEAN
TEST
VERSION
)
type (
// The Revel command type
COMMAND int
// The Command config for the line input
CommandConfig struct {
Index COMMAND // The index
Verbose []bool `short:"v" long:"debug" description:"If set the logger is set to verbose"` // True if debug is active
FrameworkVersion *Version // The framework version
CommandVersion *Version // The command version
HistoricMode bool `long:"historic-run-mode" description:"If set the runmode is passed a string not json"` // True if debug is active
ImportPath string // The import path (relative to a GOPATH)
GoPath string // The GoPath
GoCmd string // The full path to the go executable
//SrcRoot string // The source root
AppPath string // The application path (absolute)
AppName string // The application name
HistoricBuildMode bool `long:"historic-build-mode" description:"If set the code is scanned using the original parsers, not the go.1.11+"` // True if debug is active
Vendored bool // True if the application is vendored
PackageResolver func(pkgName string) error // a package resolver for the config
BuildFlags []string `short:"X" long:"build-flags" description:"These flags will be used when building the application. May be specified multiple times, only applicable for Build, Run, Package, Test commands"`
GoModFlags []string `long:"gomod-flags" description:"These flags will execute go mod commands for each flag, this happens during the build process"`
New command.New `command:"new"`
Build command.Build `command:"build"`
Run command.Run `command:"run"`
Package command.Package `command:"package"`
Clean command.Clean `command:"clean"`
Test command.Test `command:"test"`
Version command.Version `command:"version"`
}
)
// Updates the import path depending on the command
func (c *CommandConfig) UpdateImportPath() error {
var importPath string
required := true
switch c.Index {
case NEW:
importPath = c.New.ImportPath
case RUN:
importPath = c.Run.ImportPath
c.Vendored = utils.Exists(filepath.Join(importPath, "go.mod"))
case BUILD:
importPath = c.Build.ImportPath
c.Vendored = utils.Exists(filepath.Join(importPath, "go.mod"))
case PACKAGE:
importPath = c.Package.ImportPath
c.Vendored = utils.Exists(filepath.Join(importPath, "go.mod"))
case CLEAN:
importPath = c.Clean.ImportPath
c.Vendored = utils.Exists(filepath.Join(importPath, "go.mod"))
case TEST:
importPath = c.Test.ImportPath
c.Vendored = utils.Exists(filepath.Join(importPath, "go.mod"))
case VERSION:
importPath = c.Version.ImportPath
required = false
}
if len(importPath) == 0 || filepath.IsAbs(importPath) || importPath[0] == '.' {
utils.Logger.Info("Import path is absolute or not specified", "path", importPath)
// Try to determine the import path from the GO paths and the command line
currentPath, err := os.Getwd()
if len(importPath) > 0 {
if importPath[0] == '.' {
// For a relative path
importPath = filepath.Join(currentPath, importPath)
}
// For an absolute path
currentPath, _ = filepath.Abs(importPath)
}
if err == nil {
for _, path := range strings.Split(build.Default.GOPATH, string(filepath.ListSeparator)) {
utils.Logger.Infof("Checking import path %s with %s", currentPath, path)
if strings.HasPrefix(currentPath, path) && len(currentPath) > len(path) + 1 {
importPath = currentPath[len(path) + 1:]
// Remove the source from the path if it is there
if len(importPath) > 4 && (strings.ToLower(importPath[0:4]) == "src/" || strings.ToLower(importPath[0:4]) == "src\\") {
importPath = importPath[4:]
} else if importPath == "src" {
if c.Index != VERSION {
return fmt.Errorf("Invlaid import path, working dir is in GOPATH root")
}
importPath = ""
}
utils.Logger.Info("Updated import path", "path", importPath)
}
}
}
}
c.ImportPath = importPath
// We need the source root determined at this point to check the setversions
c.initAppFolder()
utils.Logger.Info("Returned import path", "path", importPath)
if required && c.Index != NEW {
if err := c.SetVersions(); err != nil {
utils.Logger.Panic("Failed to fetch revel versions", "error", err)
}
if err := c.FrameworkVersion.CompatibleFramework(c); err != nil {
utils.Logger.Fatal("Compatibility Error", "message", err,
"Revel framework version", c.FrameworkVersion.String(), "Revel tool version", c.CommandVersion.String())
}
utils.Logger.Info("Revel versions", "revel-tool", c.CommandVersion.String(), "Revel Framework", c.FrameworkVersion.String())
}
if !required {
return nil
}
if len(importPath) == 0 {
return fmt.Errorf("Unable to determine import path from : %s", importPath)
}
return nil
}
func (c *CommandConfig) initAppFolder() (err error) {
utils.Logger.Info("initAppFolder", "vendored", c.Vendored, "build-gopath", build.Default.GOPATH, "gopath-env", os.Getenv("GOPATH"))
// check for go executable
c.GoCmd, err = exec.LookPath("go")
if err != nil {
utils.Logger.Fatal("Go executable not found in PATH.")
}
// First try to determine where the application is located - this should be the import value
appFolder := c.ImportPath
wd, err := os.Getwd()
if len(appFolder) == 0 {
// We will assume the working directory is the appFolder
appFolder = wd
} else if strings.LastIndex(wd, appFolder) == len(wd) - len(appFolder) {
// Check for existence of an /app folder
if utils.Exists(filepath.Join(wd, "app")) {
appFolder = wd
} else {
appFolder = filepath.Join(wd, appFolder)
}
} else if strings.Contains(appFolder, ".") {
appFolder = filepath.Join(wd, filepath.Base(c.ImportPath))
} else if !filepath.IsAbs(appFolder) {
appFolder = filepath.Join(wd, appFolder)
}
utils.Logger.Info("Determined app folder to be", "appfolder", appFolder, "working", wd, "importPath", c.ImportPath)
// Use app folder to read the go.mod if it exists and extract the package information
goModFile := filepath.Join(appFolder, "go.mod")
utils.Logger.Info("Checking gomod, extracting from file", "path", goModFile,"exists", utils.Exists(goModFile))
if utils.Exists(goModFile) {
c.Vendored = true
utils.Logger.Info("Found go mod, extracting from file", "path", goModFile)
file, err := ioutil.ReadFile(goModFile)
if err != nil {
return err
}
for _, line := range strings.Split(string(file), "\n") {
if strings.Index(line, "module ") == 0 {
c.ImportPath = strings.TrimSpace(strings.Split(line, "module")[1])
c.AppPath = appFolder
//c.SrcRoot = appFolder
utils.Logger.Info("Set application path and package based on go mod", "path", c.AppPath)
return nil
}
}
// c.SrcRoot = appFolder
c.AppPath = appFolder
} else if c.Index != NEW || (c.Index == NEW && c.New.NotVendored) {
workingDir, _ := os.Getwd()
goPathList := filepath.SplitList(c.GoPath)
bestpath := ""
for _, path := range goPathList {
if c.Index == NEW {
// If the GOPATH is part of the working dir this is the most likely target
if strings.HasPrefix(workingDir, path) {
bestpath = path
}
} else {
if utils.Exists(filepath.Join(path, "src", c.ImportPath)) {
bestpath = path
break
}
}
}
utils.Logger.Info("Source root", "cwd", workingDir, "gopath", c.GoPath, "c.ImportPath", c.ImportPath, "bestpath", bestpath)
if len(bestpath) > 0 {
c.AppPath = filepath.Join(bestpath, "src", c.ImportPath)
}
// Recalculate the appFolder because we are using a GOPATH
} else {
// This is new and not vendored, so the app path is the appFolder
c.AppPath = appFolder
}
utils.Logger.Info("Set application path", "path", c.AppPath, "vendored",c.Vendored, "importpath",c.ImportPath)
return nil
}
// Used to initialize the package resolver
func (c *CommandConfig) InitPackageResolver() {
c.initGoPaths()
utils.Logger.Info("InitPackageResolver", "useVendor", c.Vendored, "path", c.AppPath)
// This should get called when needed
c.PackageResolver = func(pkgName string) error {
utils.Logger.Info("Request for package ", "package", pkgName, "use vendor", c.Vendored)
var getCmd *exec.Cmd
print("Downloading related packages ...")
if c.Vendored {
getCmd = exec.Command(c.GoCmd, "mod", "tidy")
} else {
utils.Logger.Info("No vendor folder detected, not using dependency manager to import package", "package", pkgName)
getCmd = exec.Command(c.GoCmd, "get", "-u", pkgName)
}
utils.CmdInit(getCmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Go get command ", "exec", getCmd.Path, "dir", getCmd.Dir, "args", getCmd.Args, "env", getCmd.Env, "package", pkgName)
output, err := getCmd.CombinedOutput()
if err != nil {
utils.Logger.Error("Failed to import package", "error", err, "gopath", build.Default.GOPATH, "GO-ROOT", build.Default.GOROOT, "output", string(output))
}
println(" completed.")
return nil
}
}
// lookup and set Go related variables
func (c *CommandConfig) initGoPaths() {
utils.Logger.Info("InitGoPaths", "vendored", c.Vendored)
// check for go executable
var err error
c.GoCmd, err = exec.LookPath("go")
if err != nil {
utils.Logger.Fatal("Go executable not found in PATH.")
}
if c.Vendored {
return
}
// lookup go path
c.GoPath = build.Default.GOPATH
if c.GoPath == "" {
utils.Logger.Fatal("Abort: GOPATH environment variable is not set. " +
"Please refer to http://golang.org/doc/code.html to configure your Go environment.")
}
return
//todo determine if the rest needs to happen
// revel/revel#1004 choose go path relative to current working directory
// What we want to do is to add the import to the end of the
// gopath, and discover which import exists - If none exist this is an error except in the case
// where we are dealing with new which is a special case where we will attempt to target the working directory first
/*
// If source root is empty and this isn't a version then skip it
if len(c.SrcRoot) == 0 {
if c.Index == NEW {
c.SrcRoot = c.New.ImportPath
} else {
if c.Index != VERSION {
utils.Logger.Fatal("Abort: could not create a Revel application outside of GOPATH.")
}
return
}
}
// set go src path
c.SrcRoot = filepath.Join(c.SrcRoot, "src")
c.AppPath = filepath.Join(c.SrcRoot, filepath.FromSlash(c.ImportPath))
utils.Logger.Info("Set application path", "path", c.AppPath)
*/
}
// Sets the versions on the command config
func (c *CommandConfig) SetVersions() (err error) {
c.CommandVersion, _ = ParseVersion(cmd.Version)
pathMap, err := utils.FindSrcPaths(c.AppPath, []string{RevelImportPath}, c.PackageResolver)
if err == nil {
utils.Logger.Info("Fullpath to revel", "dir", pathMap[RevelImportPath])
fset := token.NewFileSet() // positions are relative to fset
versionData, err := ioutil.ReadFile(filepath.Join(pathMap[RevelImportPath], "version.go"))
if err != nil {
utils.Logger.Error("Failed to find Revel version:", "error", err, "path", pathMap[RevelImportPath])
}
// Parse src but stop after processing the imports.
f, err := parser.ParseFile(fset, "", versionData, parser.ParseComments)
if err != nil {
return utils.NewBuildError("Failed to parse Revel version error:", "error", err)
}
// Print the imports from the file's AST.
for _, s := range f.Decls {
genDecl, ok := s.(*ast.GenDecl)
if !ok {
continue
}
if genDecl.Tok != token.CONST {
continue
}
for _, a := range genDecl.Specs {
spec := a.(*ast.ValueSpec)
r := spec.Values[0].(*ast.BasicLit)
if spec.Names[0].Name == "Version" {
c.FrameworkVersion, err = ParseVersion(strings.Replace(r.Value, `"`, ``, -1))
if err != nil {
utils.Logger.Errorf("Failed to parse version")
} else {
utils.Logger.Info("Parsed revel version", "version", c.FrameworkVersion.String())
}
}
}
}
}
return
}

View File

@@ -0,0 +1,11 @@
package model
// The embedded type name takes the import path and structure name
type EmbeddedTypeName struct {
ImportPath, StructName string
}
// Convert the type to a properly formatted import line
func (s *EmbeddedTypeName) String() string {
return s.ImportPath + "." + s.StructName
}

62
model/event.go Normal file
View File

@@ -0,0 +1,62 @@
package model
type (
// The event type
Event int
// The event response
EventResponse int
// The handler signature
EventHandler func(typeOf Event, value interface{}) (responseOf EventResponse)
RevelCallback interface {
FireEvent(key Event, value interface{}) (response EventResponse)
PackageResolver(pkgName string) error
}
)
const (
// Event type when templates are going to be refreshed (receivers are registered template engines added to the template.engine conf option)
TEMPLATE_REFRESH_REQUESTED Event = iota
// Event type when templates are refreshed (receivers are registered template engines added to the template.engine conf option)
TEMPLATE_REFRESH_COMPLETED
// Event type before all module loads, events thrown to handlers added to AddInitEventHandler
// Event type before all module loads, events thrown to handlers added to AddInitEventHandler
REVEL_BEFORE_MODULES_LOADED
// Event type before module loads, events thrown to handlers added to AddInitEventHandler
REVEL_BEFORE_MODULE_LOADED
// Event type after module loads, events thrown to handlers added to AddInitEventHandler
REVEL_AFTER_MODULE_LOADED
// Event type after all module loads, events thrown to handlers added to AddInitEventHandler
REVEL_AFTER_MODULES_LOADED
// Event type before server engine is initialized, receivers are active server engine and handlers added to AddInitEventHandler
ENGINE_BEFORE_INITIALIZED
// Event type before server engine is started, receivers are active server engine and handlers added to AddInitEventHandler
ENGINE_STARTED
// Event type after server engine is stopped, receivers are active server engine and handlers added to AddInitEventHandler
ENGINE_SHUTDOWN
// Called before routes are refreshed
ROUTE_REFRESH_REQUESTED
// Called after routes have been refreshed
ROUTE_REFRESH_COMPLETED
// Fired when a panic is caught during the startup process
REVEL_FAILURE
)
var initEventList = []EventHandler{} // Event handler list for receiving events
// Fires system events from revel
func RaiseEvent(key Event, value interface{}) (response EventResponse) {
for _, handler := range initEventList {
response |= handler(key, value)
}
return
}
// Add event handler to listen for all system events
func AddInitEventHandler(handler EventHandler) {
initEventList = append(initEventList, handler)
return
}

24
model/event_test.go Normal file
View File

@@ -0,0 +1,24 @@
package model_test
import (
"github.com/revel/revel"
"github.com/stretchr/testify/assert"
"testing"
)
// Test that the event handler can be attached and it dispatches the event received
func TestEventHandler(t *testing.T) {
counter := 0
newListener := func(typeOf revel.Event, value interface{}) (responseOf revel.EventResponse) {
if typeOf == revel.ENGINE_SHUTDOWN_REQUEST {
counter++
}
return
}
// Attach the same handler twice so we expect to see the response twice as well
revel.AddInitEventHandler(newListener)
revel.AddInitEventHandler(newListener)
revel.StopServer(1)
assert.Equal(t, counter, 2, "Expected event handler to have been called")
}

23
model/method.go Normal file
View File

@@ -0,0 +1,23 @@
package model
// methodCall describes a call to c.Render(..)
// It documents the argument names used, in order to propagate them to RenderArgs.
type MethodCall struct {
Path string // e.g. "myapp/app/controllers.(*Application).Action"
Line int
Names []string
}
// MethodSpec holds the information of one Method
type MethodSpec struct {
Name string // Name of the method, e.g. "Index"
Args []*MethodArg // Argument descriptors
RenderCalls []*MethodCall // Descriptions of Render() invocations from this Method.
}
// MethodArg holds the information of one argument
type MethodArg struct {
Name string // Name of the argument.
TypeExpr TypeExpr // The name of the type, e.g. "int", "*pkg.UserType"
ImportPath string // If the arg is of an imported type, this is the import path.
}

290
model/revel_container.go Normal file
View File

@@ -0,0 +1,290 @@
// This package will be shared between Revel and Revel CLI eventually
package model
import (
"github.com/revel/cmd/utils"
"github.com/revel/config"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"golang.org/x/tools/go/packages"
)
type (
// The container object for describing all Revels variables
RevelContainer struct {
BuildPaths struct {
Revel string
}
Paths struct {
Import string
Source string
Base string
App string
Views string
Code []string
Template []string
Config []string
}
PackageInfo struct {
Config config.Context
Packaged bool
DevMode bool
Vendor bool
}
Application struct {
Name string
Root string
}
ImportPath string // The import path
SourcePath string // The full source path
RunMode string // The current run mode
RevelPath string // The path to the Revel source code
BasePath string // The base path to the application
AppPath string // The application path (BasePath + "/app")
ViewsPath string // The application views path
CodePaths []string // All the code paths
TemplatePaths []string // All the template paths
ConfPaths []string // All the configuration paths
Config *config.Context // The global config object
Packaged bool // True if packaged
DevMode bool // True if running in dev mode
HTTPPort int // The http port
HTTPAddr string // The http address
HTTPSsl bool // True if running https
HTTPSslCert string // The SSL certificate
HTTPSslKey string // The SSL key
AppName string // The application name
AppRoot string // The application root from the config `app.root`
CookiePrefix string // The cookie prefix
CookieDomain string // The cookie domain
CookieSecure bool // True if cookie is secure
SecretStr string // The secret string
MimeConfig *config.Context // The mime configuration
ModulePathMap map[string]*ModuleInfo // The module path map
}
ModuleInfo struct {
ImportPath string
Path string
}
WrappedRevelCallback struct {
FireEventFunction func(key Event, value interface{}) (response EventResponse)
ImportFunction func(pkgName string) error
}
)
// Simple Wrapped RevelCallback
func NewWrappedRevelCallback(fe func(key Event, value interface{}) (response EventResponse), ie func(pkgName string) error) RevelCallback {
return &WrappedRevelCallback{fe, ie}
}
// Function to implement the FireEvent
func (w *WrappedRevelCallback) FireEvent(key Event, value interface{}) (response EventResponse) {
if w.FireEventFunction != nil {
response = w.FireEventFunction(key, value)
}
return
}
func (w *WrappedRevelCallback) PackageResolver(pkgName string) error {
return w.ImportFunction(pkgName)
}
// RevelImportPath Revel framework import path
var RevelImportPath = "github.com/revel/revel"
var RevelModulesImportPath = "github.com/revel/modules"
// This function returns a container object describing the revel application
// eventually this type of function will replace the global variables.
func NewRevelPaths(mode, importPath, appSrcPath string, callback RevelCallback) (rp *RevelContainer, err error) {
rp = &RevelContainer{ModulePathMap: map[string]*ModuleInfo{}}
// Ignore trailing slashes.
rp.ImportPath = strings.TrimRight(importPath, "/")
rp.SourcePath = appSrcPath
rp.RunMode = mode
// We always need to determine the paths for files
pathMap, err := utils.FindSrcPaths(appSrcPath, []string{importPath+"/app", RevelImportPath}, callback.PackageResolver)
if err != nil {
return
}
rp.AppPath, rp.RevelPath = pathMap[importPath], pathMap[RevelImportPath]
// Setup paths for application
rp.BasePath = rp.SourcePath
rp.PackageInfo.Vendor = utils.Exists(filepath.Join(rp.BasePath, "go.mod"))
rp.AppPath = filepath.Join(rp.BasePath, "app")
// Sanity check , ensure app and conf paths exist
if !utils.DirExists(rp.AppPath) {
return rp, fmt.Errorf("No application found at path %s", rp.AppPath)
}
if !utils.DirExists(filepath.Join(rp.BasePath, "conf")) {
return rp, fmt.Errorf("No configuration found at path %s", filepath.Join(rp.BasePath, "conf"))
}
rp.ViewsPath = filepath.Join(rp.AppPath, "views")
rp.CodePaths = []string{rp.AppPath}
rp.TemplatePaths = []string{}
if rp.ConfPaths == nil {
rp.ConfPaths = []string{}
}
// Config load order
// 1. framework (revel/conf/*)
// 2. application (conf/*)
// 3. user supplied configs (...) - User configs can override/add any from above
rp.ConfPaths = append(
[]string{
filepath.Join(rp.RevelPath, "conf"),
filepath.Join(rp.BasePath, "conf"),
},
rp.ConfPaths...)
rp.Config, err = config.LoadContext("app.conf", rp.ConfPaths)
if err != nil {
return rp, fmt.Errorf("Unable to load configuartion file %s", err)
}
// Ensure that the selected runmode appears in app.conf.
// If empty string is passed as the mode, treat it as "DEFAULT"
if mode == "" {
mode = config.DefaultSection
}
if !rp.Config.HasSection(mode) {
return rp, fmt.Errorf("app.conf: No mode found: %s %s", "run-mode", mode)
}
rp.Config.SetSection(mode)
// Configure properties from app.conf
rp.DevMode = rp.Config.BoolDefault("mode.dev", false)
rp.HTTPPort = rp.Config.IntDefault("http.port", 9000)
rp.HTTPAddr = rp.Config.StringDefault("http.addr", "")
rp.HTTPSsl = rp.Config.BoolDefault("http.ssl", false)
rp.HTTPSslCert = rp.Config.StringDefault("http.sslcert", "")
rp.HTTPSslKey = rp.Config.StringDefault("http.sslkey", "")
if rp.HTTPSsl {
if rp.HTTPSslCert == "" {
return rp, errors.New("No http.sslcert provided.")
}
if rp.HTTPSslKey == "" {
return rp, errors.New("No http.sslkey provided.")
}
}
//
rp.AppName = rp.Config.StringDefault("app.name", "(not set)")
rp.AppRoot = rp.Config.StringDefault("app.root", "")
rp.CookiePrefix = rp.Config.StringDefault("cookie.prefix", "REVEL")
rp.CookieDomain = rp.Config.StringDefault("cookie.domain", "")
rp.CookieSecure = rp.Config.BoolDefault("cookie.secure", rp.HTTPSsl)
rp.SecretStr = rp.Config.StringDefault("app.secret", "")
callback.FireEvent(REVEL_BEFORE_MODULES_LOADED, nil)
utils.Logger.Info("Loading modules")
if err := rp.loadModules(callback); err != nil {
return rp, err
}
callback.FireEvent(REVEL_AFTER_MODULES_LOADED, nil)
return
}
// LoadMimeConfig load mime-types.conf on init.
func (rp *RevelContainer) LoadMimeConfig() (err error) {
rp.MimeConfig, err = config.LoadContext("mime-types.conf", rp.ConfPaths)
if err != nil {
return fmt.Errorf("Failed to load mime type config: %s %s", "error", err)
}
return
}
// Loads modules based on the configuration setup.
// This will fire the REVEL_BEFORE_MODULE_LOADED, REVEL_AFTER_MODULE_LOADED
// for each module loaded. The callback will receive the RevelContainer, name, moduleImportPath and modulePath
// It will automatically add in the code paths for the module to the
// container object
func (rp *RevelContainer) loadModules(callback RevelCallback) (err error) {
keys := []string{}
for _, key := range rp.Config.Options("module.") {
keys = append(keys, key)
}
// Reorder module order by key name, a poor mans sort but at least it is consistent
sort.Strings(keys)
for _, key := range keys {
moduleImportPath := rp.Config.StringDefault(key, "")
if moduleImportPath == "" {
continue
}
modulePath, err := rp.ResolveImportPath(moduleImportPath)
if err != nil {
utils.Logger.Info("Missing module ", "module_import_path", moduleImportPath, "error",err)
callback.PackageResolver(moduleImportPath)
modulePath, err = rp.ResolveImportPath(moduleImportPath)
if err != nil {
return fmt.Errorf("Failed to load module. Import of path failed %s:%s %s:%s ", "modulePath", moduleImportPath, "error", err)
}
}
// Drop anything between module.???.<name of module>
name := key[len("module."):]
if index := strings.Index(name, "."); index > -1 {
name = name[index+1:]
}
callback.FireEvent(REVEL_BEFORE_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath})
rp.addModulePaths(name, moduleImportPath, modulePath)
callback.FireEvent(REVEL_AFTER_MODULE_LOADED, []interface{}{rp, name, moduleImportPath, modulePath})
}
return
}
// Adds a module paths to the container object
func (rp *RevelContainer) addModulePaths(name, importPath, modulePath string) {
utils.Logger.Info("Adding module path","name", name,"import path", importPath,"system path", modulePath)
if codePath := filepath.Join(modulePath, "app"); utils.DirExists(codePath) {
rp.CodePaths = append(rp.CodePaths, codePath)
rp.ModulePathMap[name] = &ModuleInfo{importPath, modulePath}
if viewsPath := filepath.Join(modulePath, "app", "views"); utils.DirExists(viewsPath) {
rp.TemplatePaths = append(rp.TemplatePaths, viewsPath)
}
}
// Hack: There is presently no way for the testrunner module to add the
// "test" subdirectory to the CodePaths. So this does it instead.
if importPath == rp.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") {
joinedPath := filepath.Join(rp.BasePath, "tests")
rp.CodePaths = append(rp.CodePaths, joinedPath)
}
if testsPath := filepath.Join(modulePath, "tests"); utils.DirExists(testsPath) {
rp.CodePaths = append(rp.CodePaths, testsPath)
}
}
// ResolveImportPath returns the filesystem path for the given import path.
// Returns an error if the import path could not be found.
func (rp *RevelContainer) ResolveImportPath(importPath string) (string, error) {
if rp.Packaged {
return filepath.Join(rp.SourcePath, importPath), nil
}
config := &packages.Config{
Mode: packages.LoadSyntax,
Dir:rp.AppPath,
}
pkgs, err := packages.Load(config, importPath)
if len(pkgs)==0 {
return "", errors.New("No packages found for import " + importPath +" using app path "+ rp.AppPath)
}
// modPkg, err := build.Import(importPath, rp.AppPath, build.FindOnly)
if err != nil {
return "", err
}
if len(pkgs[0].GoFiles)>0 {
return filepath.Dir(pkgs[0].GoFiles[0]), nil
}
return pkgs[0].PkgPath, errors.New("No files found in import path " + importPath)
}

139
model/source_info.go Normal file
View File

@@ -0,0 +1,139 @@
package model
// SourceInfo is the top-level struct containing all extracted information
// about the app source code, used to generate main.go.
import (
"github.com/revel/cmd/utils"
"path/filepath"
"strings"
"unicode"
)
type SourceInfo struct {
// StructSpecs lists type info for all structs found under the code paths.
// They may be queried to determine which ones (transitively) embed certain types.
StructSpecs []*TypeInfo
// ValidationKeys provides a two-level lookup. The keys are:
// 1. The fully-qualified function name,
// e.g. "github.com/revel/examples/chat/app/controllers.(*Application).Action"
// 2. Within that func's file, the line number of the (overall) expression statement.
// e.g. the line returned from runtime.Caller()
// The result of the lookup the name of variable being validated.
ValidationKeys map[string]map[int]string
// A list of import paths.
// Revel notices files with an init() function and imports that package.
InitImportPaths []string
// controllerSpecs lists type info for all structs found under
// app/controllers/... that embed (directly or indirectly) revel.Controller
controllerSpecs []*TypeInfo
// testSuites list the types that constitute the set of application tests.
testSuites []*TypeInfo
// packageMap a map of import to system directory (if available)
PackageMap map[string]string
}
// TypesThatEmbed returns all types that (directly or indirectly) embed the
// target type, which must be a fully qualified type name,
// e.g. "github.com/revel/revel.Controller"
func (s *SourceInfo) TypesThatEmbed(targetType, packageFilter string) (filtered []*TypeInfo) {
// Do a search in the "embedded type graph", starting with the target type.
var (
nodeQueue = []string{targetType}
processed []string
)
for len(nodeQueue) > 0 {
typeSimpleName := nodeQueue[0]
nodeQueue = nodeQueue[1:]
processed = append(processed, typeSimpleName)
// Look through all known structs.
for _, spec := range s.StructSpecs {
// If this one has been processed or is already in nodeQueue, then skip it.
if utils.ContainsString(processed, spec.String()) ||
utils.ContainsString(nodeQueue, spec.String()) {
continue
}
// Look through the embedded types to see if the current type is among them.
for _, embeddedType := range spec.EmbeddedTypes {
// If so, add this type's simple name to the nodeQueue, and its spec to
// the filtered list.
if typeSimpleName == embeddedType.String() {
nodeQueue = append(nodeQueue, spec.String())
filtered = append(filtered, spec)
break
}
}
}
}
// Strip out any specifications that contain a lower case
for exit := false; !exit; exit = true {
for i, filteredItem := range filtered {
if unicode.IsLower([]rune(filteredItem.StructName)[0]) {
utils.Logger.Info("Debug: Skipping adding spec for unexported type",
"type", filteredItem.StructName,
"package", filteredItem.ImportPath)
filtered = append(filtered[:i], filtered[i + 1:]...)
exit = false
break
}
}
}
// Check for any missed types that where from expected packages
for _, spec := range s.StructSpecs {
if spec.PackageName == packageFilter {
found := false
unfoundNames := ""
for _, filteredItem := range filtered {
if filteredItem.StructName == spec.StructName {
found = true
break
} else {
unfoundNames += filteredItem.StructName + ","
}
}
// Report non controller structures in controller folder.
if !found && !strings.HasPrefix(spec.StructName, "Test") {
utils.Logger.Warn("Type found in package: " + packageFilter +
", but did not embed from: " + filepath.Base(targetType),
"name", spec.StructName, "importpath", spec.ImportPath, "foundstructures", unfoundNames)
}
}
}
return
}
// ControllerSpecs returns the all the controllers that embeds
// `revel.Controller`
func (s *SourceInfo) ControllerSpecs() []*TypeInfo {
utils.Logger.Info("Scanning controller specifications for types ","typePath",RevelImportPath + ".Controller", "speclen",len(s.controllerSpecs))
if s.controllerSpecs == nil {
s.controllerSpecs = s.TypesThatEmbed(RevelImportPath + ".Controller", "controllers")
}
return s.controllerSpecs
}
// TestSuites returns the all the Application tests that embeds
// `testing.TestSuite`
func (s *SourceInfo) TestSuites() []*TypeInfo {
if s.testSuites == nil {
s.testSuites = s.TypesThatEmbed(RevelImportPath + "/testing.TestSuite", "testsuite")
}
return s.testSuites
}
func (s *SourceInfo) Merge(srcInfo2 *SourceInfo) {
s.StructSpecs = append(s.StructSpecs, srcInfo2.StructSpecs...)
s.InitImportPaths = append(s.InitImportPaths, srcInfo2.InitImportPaths...)
for k, v := range srcInfo2.ValidationKeys {
if _, ok := s.ValidationKeys[k]; ok {
utils.Logger.Warn("Warn: Key conflict when scanning validation calls:", "key", k)
continue
}
s.ValidationKeys[k] = v
}
}

102
model/type_expr.go Normal file
View File

@@ -0,0 +1,102 @@
package model
// TypeExpr provides a type name that may be rewritten to use a package name.
import (
"fmt"
"go/ast"
)
type TypeExpr struct {
Expr string // The unqualified type expression, e.g. "[]*MyType"
PkgName string // The default package idenifier
pkgIndex int // The index where the package identifier should be inserted.
Valid bool
}
// Returns a new type from the data
func NewTypeExprFromData(expr, pkgName string, pkgIndex int, valid bool) TypeExpr {
return TypeExpr{expr, pkgName, pkgIndex, valid}
}
// NewTypeExpr returns the syntactic expression for referencing this type in Go.
func NewTypeExprFromAst(pkgName string, expr ast.Expr) TypeExpr {
error := ""
switch t := expr.(type) {
case *ast.Ident:
if IsBuiltinType(t.Name) {
pkgName = ""
}
return TypeExpr{t.Name, pkgName, 0, true}
case *ast.SelectorExpr:
e := NewTypeExprFromAst(pkgName, t.X)
return NewTypeExprFromData(t.Sel.Name, e.Expr, 0, e.Valid)
case *ast.StarExpr:
e := NewTypeExprFromAst(pkgName, t.X)
return NewTypeExprFromData("*"+e.Expr, e.PkgName, e.pkgIndex+1, e.Valid)
case *ast.ArrayType:
e := NewTypeExprFromAst(pkgName, t.Elt)
return NewTypeExprFromData("[]"+e.Expr, e.PkgName, e.pkgIndex+2, e.Valid)
case *ast.MapType:
if identKey, ok := t.Key.(*ast.Ident); ok && IsBuiltinType(identKey.Name) {
e := NewTypeExprFromAst(pkgName, t.Value)
return NewTypeExprFromData("map["+identKey.Name+"]"+e.Expr, e.PkgName, e.pkgIndex+len("map["+identKey.Name+"]"), e.Valid)
}
error = fmt.Sprintf("Failed to generate name for Map field :%v. Make sure the field name is valid.", t.Key)
case *ast.Ellipsis:
e := NewTypeExprFromAst(pkgName, t.Elt)
return NewTypeExprFromData("[]"+e.Expr, e.PkgName, e.pkgIndex+2, e.Valid)
default:
error = fmt.Sprintf("Failed to generate name for field: %v Package: %v. Make sure the field name is valid.", expr, pkgName)
}
return NewTypeExprFromData(error, "", 0, false)
}
// TypeName returns the fully-qualified type name for this expression.
// The caller may optionally specify a package name to override the default.
func (e TypeExpr) TypeName(pkgOverride string) string {
pkgName := FirstNonEmpty(pkgOverride, e.PkgName)
if pkgName == "" {
return e.Expr
}
return e.Expr[:e.pkgIndex] + pkgName + "." + e.Expr[e.pkgIndex:]
}
var builtInTypes = map[string]struct{}{
"bool": {},
"byte": {},
"complex128": {},
"complex64": {},
"error": {},
"float32": {},
"float64": {},
"int": {},
"int16": {},
"int32": {},
"int64": {},
"int8": {},
"rune": {},
"string": {},
"uint": {},
"uint16": {},
"uint32": {},
"uint64": {},
"uint8": {},
"uintptr": {},
}
// IsBuiltinType checks the given type is built-in types of Go
func IsBuiltinType(name string) bool {
_, ok := builtInTypes[name]
return ok
}
// Returns the first non empty string from a list of arguements
func FirstNonEmpty(strs ...string) string {
for _, str := range strs {
if len(str) > 0 {
return str
}
}
return ""
}

15
model/type_info.go Normal file
View File

@@ -0,0 +1,15 @@
package model
// TypeInfo summarizes information about a struct type in the app source code.
type TypeInfo struct {
StructName string // e.g. "Application"
ImportPath string // e.g. "github.com/revel/examples/chat/app/controllers"
PackageName string // e.g. "controllers"
MethodSpecs []*MethodSpec // Method specifications, the action functions
EmbeddedTypes []*EmbeddedTypeName // Used internally to identify controllers that indirectly embed *revel.Controller.
}
// Return the type information as a properly formatted import string
func (s *TypeInfo) String() string {
return s.ImportPath + "." + s.StructName
}

126
model/version.go Normal file
View File

@@ -0,0 +1,126 @@
package model
import (
"fmt"
"github.com/pkg/errors"
"regexp"
"strconv"
)
type Version struct {
Prefix string
Major int
Minor int
Maintenance int
Suffix string
BuildDate string
MinGoVersion string
}
// The compatibility list
var frameworkCompatibleRangeList = [][]string{
{"0.0.0", "0.20.0"}, // minimum Revel version to use with this version of the tool
{"0.19.99", "0.30.0"}, // Compatible with Framework V 0.19.99 - 0.30.0
{"1.0.0", "1.1.0"}, // Compatible with Framework V 1.0 - 1.1
}
// Parses a version like v1.2.3a or 1.2
var versionRegExp = regexp.MustCompile(`([^\d]*)?([0-9]*)\.([0-9]*)(\.([0-9]*))?(.*)`)
// Parse the version and return it as a Version object
func ParseVersion(version string) (v *Version, err error) {
v = &Version{}
return v, v.ParseVersion(version)
}
// Parse the version and return it as a Version object
func (v *Version)ParseVersion(version string) (err error) {
parsedResult := versionRegExp.FindAllStringSubmatch(version, -1)
if len(parsedResult) != 1 {
err = errors.Errorf("Invalid version %s", version)
return
}
if len(parsedResult[0]) != 7 {
err = errors.Errorf("Invalid version %s", version)
return
}
v.Prefix = parsedResult[0][1]
v.Major = v.intOrZero(parsedResult[0][2])
v.Minor = v.intOrZero(parsedResult[0][3])
v.Maintenance = v.intOrZero(parsedResult[0][5])
v.Suffix = parsedResult[0][6]
return
}
// Returns 0 or an int value for the string, errors are returned as 0
func (v *Version) intOrZero(input string) (value int) {
if input != "" {
value, _ = strconv.Atoi(input)
}
return value
}
// Returns true if this major revision is compatible
func (v *Version) CompatibleFramework(c *CommandConfig) error {
for i, rv := range frameworkCompatibleRangeList {
start, _ := ParseVersion(rv[0])
end, _ := ParseVersion(rv[1])
if !v.Newer(start) || v.Newer(end) {
continue
}
// Framework is older then 0.20, turn on historic mode
if i == 0 {
c.HistoricMode = true
}
return nil
}
return errors.New("Tool out of date - do a 'go get -u github.com/revel/cmd/revel'")
}
// Returns true if this major revision is newer then the passed in
func (v *Version) MajorNewer(o *Version) bool {
if v.Major != o.Major {
return v.Major > o.Major
}
return false
}
// Returns true if this major or major and minor revision is newer then the value passed in
func (v *Version) MinorNewer(o *Version) bool {
if v.Major != o.Major {
return v.Major > o.Major
}
if v.Minor != o.Minor {
return v.Minor > o.Minor
}
return false
}
// Returns true if the version is newer then the current on
func (v *Version) Newer(o *Version) bool {
if v.Major != o.Major {
return v.Major > o.Major
}
if v.Minor != o.Minor {
return v.Minor > o.Minor
}
if v.Maintenance != o.Maintenance {
return v.Maintenance > o.Maintenance
}
return true
}
// Convert the version to a string
func (v *Version) VersionString() string {
return fmt.Sprintf("%s%d.%d.%d%s", v.Prefix, v.Major, v.Minor, v.Maintenance, v.Suffix)
}
// Convert the version build date and go version to a string
func (v *Version) String() string {
return fmt.Sprintf("Version: %s%d.%d.%d%s\nBuild Date: %s\n Minimium Go Version: %s",
v.Prefix, v.Major, v.Minor, v.Maintenance, v.Suffix, v.BuildDate, v.MinGoVersion)
}

33
model/version_test.go Normal file
View File

@@ -0,0 +1,33 @@
package model_test
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/revel/cmd/model"
)
var versionTests = [][]string{
{"v0.20.0-dev", "v0.20.0-dev"},
{"v0.20-dev", "v0.20.0-dev"},
{"v0.20.", "v0.20.0"},
{"2.0", "2.0.0"},
}
// Test that the event handler can be attached and it dispatches the event received
func TestVersion(t *testing.T) {
for _, v:= range versionTests {
p,e:=model.ParseVersion(v[0])
assert.Nil(t,e,"Should have parsed %s",v)
assert.Equal(t,p.String(),v[1], "Should be equal %s==%s",p.String(),v)
}
}
// test the ranges
func TestVersionRange(t *testing.T) {
a,_ := model.ParseVersion("0.1.2")
b,_ := model.ParseVersion("0.2.1")
c,_ := model.ParseVersion("1.0.1")
assert.True(t, b.MinorNewer(a), "B is newer then A")
assert.False(t, b.MajorNewer(a), "B is not major newer then A")
assert.False(t, b.MajorNewer(c), "B is not major newer then A")
assert.True(t, c.MajorNewer(b), "C is major newer then b")
}

223
parser/appends.go Normal file
View File

@@ -0,0 +1,223 @@
package parser
import (
"go/ast"
"github.com/revel/cmd/utils"
"github.com/revel/cmd/model"
"go/token"
)
// If this Decl is a struct type definition, it is summarized and added to specs.
// Else, specs is returned unchanged.
func appendStruct(fileName string, specs []*model.TypeInfo, pkgImportPath string, pkg *ast.Package, decl ast.Decl, imports map[string]string, fset *token.FileSet) []*model.TypeInfo {
// Filter out non-Struct type declarations.
spec, found := getStructTypeDecl(decl, fset)
if !found {
return specs
}
structType := spec.Type.(*ast.StructType)
// At this point we know it's a type declaration for a struct.
// Fill in the rest of the info by diving into the fields.
// Add it provisionally to the Controller list -- it's later filtered using field info.
controllerSpec := &model.TypeInfo{
StructName: spec.Name.Name,
ImportPath: pkgImportPath,
PackageName: pkg.Name,
}
for _, field := range structType.Fields.List {
// If field.Names is set, it's not an embedded type.
if field.Names != nil {
continue
}
// A direct "sub-type" has an ast.Field as either:
// Ident { "AppController" }
// SelectorExpr { "rev", "Controller" }
// Additionally, that can be wrapped by StarExprs.
fieldType := field.Type
pkgName, typeName := func() (string, string) {
// Drill through any StarExprs.
for {
if starExpr, ok := fieldType.(*ast.StarExpr); ok {
fieldType = starExpr.X
continue
}
break
}
// If the embedded type is in the same package, it's an Ident.
if ident, ok := fieldType.(*ast.Ident); ok {
return "", ident.Name
}
if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok {
if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok {
return pkgIdent.Name, selectorExpr.Sel.Name
}
}
return "", ""
}()
// If a typename wasn't found, skip it.
if typeName == "" {
continue
}
// Find the import path for this type.
// If it was referenced without a package name, use the current package import path.
// Else, look up the package's import path by name.
var importPath string
if pkgName == "" {
importPath = pkgImportPath
} else {
var ok bool
if importPath, ok = imports[pkgName]; !ok {
utils.Logger.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName)
continue
}
}
controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{
ImportPath: importPath,
StructName: typeName,
})
}
return append(specs, controllerSpec)
}
// If decl is a Method declaration, it is summarized and added to the array
// underneath its receiver type.
// e.g. "Login" => {MethodSpec, MethodSpec, ..}
func appendAction(fset *token.FileSet, mm methodMap, decl ast.Decl, pkgImportPath, pkgName string, imports map[string]string) {
// Func declaration?
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok {
return
}
// Have a receiver?
if funcDecl.Recv == nil {
return
}
// Is it public?
if !funcDecl.Name.IsExported() {
return
}
// Does it return a Result?
if funcDecl.Type.Results == nil || len(funcDecl.Type.Results.List) != 1 {
return
}
selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr)
if !ok {
return
}
if selExpr.Sel.Name != "Result" {
return
}
if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || imports[pkgIdent.Name] != model.RevelImportPath {
return
}
method := &model.MethodSpec{
Name: funcDecl.Name.Name,
}
// Add a description of the arguments to the method.
for _, field := range funcDecl.Type.Params.List {
for _, name := range field.Names {
var importPath string
typeExpr := model.NewTypeExprFromAst(pkgName, field.Type)
if !typeExpr.Valid {
utils.Logger.Warnf("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, getFuncName(funcDecl))
return // We didn't understand one of the args. Ignore this action.
}
// Local object
if typeExpr.PkgName == pkgName {
importPath = pkgImportPath
} else if typeExpr.PkgName != "" {
var ok bool
if importPath, ok = imports[typeExpr.PkgName]; !ok {
utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName(""))
}
}
method.Args = append(method.Args, &model.MethodArg{
Name: name.Name,
TypeExpr: typeExpr,
ImportPath: importPath,
})
}
}
// Add a description of the calls to Render from the method.
// Inspect every node (e.g. always return true).
method.RenderCalls = []*model.MethodCall{}
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// Is it a function call?
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// Is it calling (*Controller).Render?
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// The type of the receiver is not easily available, so just store every
// call to any method called Render.
if selExpr.Sel.Name != "Render" {
return true
}
// Add this call's args to the renderArgs.
pos := fset.Position(callExpr.Lparen)
methodCall := &model.MethodCall{
Line: pos.Line,
Names: []string{},
}
for _, arg := range callExpr.Args {
argIdent, ok := arg.(*ast.Ident)
if !ok {
continue
}
methodCall.Names = append(methodCall.Names, argIdent.Name)
}
method.RenderCalls = append(method.RenderCalls, methodCall)
return true
})
var recvTypeName string
var recvType = funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
recvTypeName = recvStarType.X.(*ast.Ident).Name
} else {
recvTypeName = recvType.(*ast.Ident).Name
}
mm[recvTypeName] = append(mm[recvTypeName], method)
}
// Combine the 2 source info models into one
func appendSourceInfo(srcInfo1, srcInfo2 *model.SourceInfo) *model.SourceInfo {
if srcInfo1 == nil {
return srcInfo2
}
srcInfo1.StructSpecs = append(srcInfo1.StructSpecs, srcInfo2.StructSpecs...)
srcInfo1.InitImportPaths = append(srcInfo1.InitImportPaths, srcInfo2.InitImportPaths...)
for k, v := range srcInfo2.ValidationKeys {
if _, ok := srcInfo1.ValidationKeys[k]; ok {
utils.Logger.Warn("Warn: Key conflict when scanning validation calls:", "key", k)
continue
}
srcInfo1.ValidationKeys[k] = v
}
return srcInfo1
}

87
parser/imports.go Normal file
View File

@@ -0,0 +1,87 @@
package parser
import (
"github.com/revel/cmd/utils"
"go/ast"
"go/build"
"go/token"
"path/filepath"
"strings"
)
// Add imports to the map from the source dir
func addImports(imports map[string]string, decl ast.Decl, srcDir string) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.IMPORT {
return
}
for _, spec := range genDecl.Specs {
importSpec := spec.(*ast.ImportSpec)
var pkgAlias string
if importSpec.Name != nil {
pkgAlias = importSpec.Name.Name
if pkgAlias == "_" {
continue
}
}
quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\""
if quotedPath == `"C"` {
continue
}
fullPath := quotedPath[1 : len(quotedPath)-1] // Remove the quotes
// If the package was not aliased (common case), we have to import it
// to see what the package name is.
// TODO: Can improve performance here a lot:
// 1. Do not import everything over and over again. Keep a cache.
// 2. Exempt the standard library; their directories always match the package name.
// 3. Can use build.FindOnly and then use parser.ParseDir with mode PackageClauseOnly
if pkgAlias == "" {
utils.Logger.Debug("Reading from build", "path", fullPath, "srcPath", srcDir, "gopath", build.Default.GOPATH)
pkg, err := build.Import(fullPath, srcDir, 0)
if err != nil {
// We expect this to happen for apps using reverse routing (since we
// have not yet generated the routes). Don't log that.
if !strings.HasSuffix(fullPath, "/app/routes") {
utils.Logger.Warn("Could not find import:", "path", fullPath, "srcPath", srcDir, "error", err)
}
continue
} else {
utils.Logger.Debug("Found package in dir", "dir", pkg.Dir, "name", pkg.ImportPath)
}
pkgAlias = pkg.Name
}
imports[pkgAlias] = fullPath
}
}
// Returns a valid import string from the path
// using the build.Defaul.GOPATH to determine the root
func importPathFromPath(root, basePath string) string {
vendorTest := filepath.Join(basePath, "vendor")
if len(root) > len(vendorTest) && root[:len(vendorTest)] == vendorTest {
return filepath.ToSlash(root[len(vendorTest)+1:])
}
for _, gopath := range filepath.SplitList(build.Default.GOPATH) {
srcPath := filepath.Join(gopath, "src")
if strings.HasPrefix(root, srcPath) {
return filepath.ToSlash(root[len(srcPath)+1:])
}
}
srcPath := filepath.Join(build.Default.GOROOT, "src", "pkg")
if strings.HasPrefix(root, srcPath) {
utils.Logger.Warn("Code path should be in GOPATH, but is in GOROOT:", "path", root)
return filepath.ToSlash(root[len(srcPath)+1:])
}
utils.Logger.Error("Unexpected! Code path is not in GOPATH:", "path", root)
return ""
}

247
parser/reflect.go Normal file
View File

@@ -0,0 +1,247 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package parser
// This file handles the app code introspection.
// It catalogs the controllers, their methods, and their arguments.
import (
"go/ast"
"go/parser"
"go/scanner"
"go/token"
"os"
"path/filepath"
"strings"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
)
// A container used to support the reflection package
type processContainer struct {
root, rootImportPath string // The paths
paths *model.RevelContainer // The Revel paths
srcInfo *model.SourceInfo // The source information container
}
// Maps a controller simple name (e.g. "Login") to the methods for which it is a
// receiver.
type methodMap map[string][]*model.MethodSpec
// ProcessSource parses the app controllers directory and
// returns a list of the controller types found.
// Otherwise CompileError if the parsing fails.
func ProcessSource(paths *model.RevelContainer) (_ *model.SourceInfo, compileError error) {
pc := &processContainer{paths: paths}
for _, root := range paths.CodePaths {
rootImportPath := importPathFromPath(root, paths.BasePath)
if rootImportPath == "" {
utils.Logger.Info("Skipping empty code path", "path", root)
continue
}
pc.root, pc.rootImportPath = root, rootImportPath
// Start walking the directory tree.
compileError = utils.Walk(root, pc.processPath)
if compileError != nil {
return
}
}
return pc.srcInfo, compileError
}
// Called during the "walk process"
func (pc *processContainer) processPath(path string, info os.FileInfo, err error) error {
if err != nil {
utils.Logger.Error("Error scanning app source:", "error", err)
return nil
}
if !info.IsDir() || info.Name() == "tmp" {
return nil
}
// Get the import path of the package.
pkgImportPath := pc.rootImportPath
if pc.root != path {
pkgImportPath = pc.rootImportPath + "/" + filepath.ToSlash(path[len(pc.root)+1:])
}
// Parse files within the path.
var pkgs map[string]*ast.Package
fset := token.NewFileSet()
pkgs, err = parser.ParseDir(
fset,
path,
func(f os.FileInfo) bool {
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
},
0)
if err != nil {
if errList, ok := err.(scanner.ErrorList); ok {
var pos = errList[0].Pos
newError := &utils.SourceError{
SourceType: ".go source",
Title: "Go Compilation Error",
Path: pos.Filename,
Description: errList[0].Msg,
Line: pos.Line,
Column: pos.Column,
SourceLines: utils.MustReadLines(pos.Filename),
}
errorLink := pc.paths.Config.StringDefault("error.link", "")
if errorLink != "" {
newError.SetLink(errorLink)
}
return newError
}
// This is exception, err already checked above. Here just a print
ast.Print(nil, err)
utils.Logger.Fatal("Failed to parse dir", "error", err)
}
// Skip "main" packages.
delete(pkgs, "main")
// Ignore packages that end with _test
// These cannot be included in source code that is not generated specifically as a test
for i := range pkgs {
if len(i) > 6 {
if string(i[len(i)-5:]) == "_test" {
delete(pkgs, i)
}
}
}
// If there is no code in this directory, skip it.
if len(pkgs) == 0 {
return nil
}
// There should be only one package in this directory.
if len(pkgs) > 1 {
for i := range pkgs {
println("Found package ", i)
}
utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgs)
}
var pkg *ast.Package
for _, v := range pkgs {
pkg = v
}
if pkg != nil {
pc.srcInfo = appendSourceInfo(pc.srcInfo, processPackage(fset, pkgImportPath, path, pkg))
} else {
utils.Logger.Info("Ignoring package, because it contained no packages", "path", path)
}
return nil
}
// Process a single package within a file
func processPackage(fset *token.FileSet, pkgImportPath, pkgPath string, pkg *ast.Package) *model.SourceInfo {
var (
structSpecs []*model.TypeInfo
initImportPaths []string
methodSpecs = make(methodMap)
validationKeys = make(map[string]map[int]string)
scanControllers = strings.HasSuffix(pkgImportPath, "/controllers") ||
strings.Contains(pkgImportPath, "/controllers/")
scanTests = strings.HasSuffix(pkgImportPath, "/tests") ||
strings.Contains(pkgImportPath, "/tests/")
)
// For each source file in the package...
utils.Logger.Info("Exaimining files in path", "package", pkgPath)
for fname, file := range pkg.Files {
// Imports maps the package key to the full import path.
// e.g. import "sample/app/models" => "models": "sample/app/models"
imports := map[string]string{}
// For each declaration in the source file...
for _, decl := range file.Decls {
addImports(imports, decl, pkgPath)
if scanControllers {
// Match and add both structs and methods
structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
appendAction(fset, methodSpecs, decl, pkgImportPath, pkg.Name, imports)
} else if scanTests {
structSpecs = appendStruct(fname, structSpecs, pkgImportPath, pkg, decl, imports, fset)
}
// If this is a func... (ignore nil for external (non-Go) function)
if funcDecl, ok := decl.(*ast.FuncDecl); ok && funcDecl.Body != nil {
// Scan it for validation calls
lineKeys := GetValidationKeys(fname, fset, funcDecl, imports)
if len(lineKeys) > 0 {
validationKeys[pkgImportPath+"."+getFuncName(funcDecl)] = lineKeys
}
// Check if it's an init function.
if funcDecl.Name.Name == "init" {
initImportPaths = []string{pkgImportPath}
}
}
}
}
// Add the method specs to the struct specs.
for _, spec := range structSpecs {
spec.MethodSpecs = methodSpecs[spec.StructName]
}
return &model.SourceInfo{
StructSpecs: structSpecs,
ValidationKeys: validationKeys,
InitImportPaths: initImportPaths,
}
}
// getFuncName returns a name for this func or method declaration.
// e.g. "(*Application).SayHello" for a method, "SayHello" for a func.
func getFuncName(funcDecl *ast.FuncDecl) string {
prefix := ""
if funcDecl.Recv != nil {
recvType := funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
} else {
prefix = recvType.(*ast.Ident).Name
}
prefix += "."
}
return prefix + funcDecl.Name.Name
}
// getStructTypeDecl checks if the given decl is a type declaration for a
// struct. If so, the TypeSpec is returned.
func getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.TYPE {
return
}
if len(genDecl.Specs) == 0 {
utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
return
}
spec = genDecl.Specs[0].(*ast.TypeSpec)
_, found = spec.Type.(*ast.StructType)
return
}

View File

@@ -2,19 +2,18 @@
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package harness
package parser_test
import (
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"log"
"reflect"
"strings"
"testing"
"github.com/revel/revel"
"github.com/revel/cmd/model"
revelParser "github.com/revel/cmd/parser"
)
const validationKeysSource = `
@@ -82,7 +81,7 @@ func TestGetValidationKeys(t *testing.T) {
}
for i, decl := range file.Decls {
lineKeys := getValidationKeys(fset, decl.(*ast.FuncDecl), map[string]string{"revel": revel.RevelImportPath})
lineKeys := revelParser.GetValidationKeys("test", fset, decl.(*ast.FuncDecl), map[string]string{"revel": model.RevelImportPath})
for k, v := range expectedValidationKeys[i] {
if lineKeys[k] != v {
t.Errorf("Not found - %d: %v - Actual Map: %v", k, v, lineKeys)
@@ -95,19 +94,21 @@ func TestGetValidationKeys(t *testing.T) {
}
}
var TypeExprs = map[string]TypeExpr{
"int": {"int", "", 0, true},
"*int": {"*int", "", 1, true},
"[]int": {"[]int", "", 2, true},
"...int": {"[]int", "", 2, true},
"[]*int": {"[]*int", "", 3, true},
"...*int": {"[]*int", "", 3, true},
"MyType": {"MyType", "pkg", 0, true},
"*MyType": {"*MyType", "pkg", 1, true},
"[]MyType": {"[]MyType", "pkg", 2, true},
"...MyType": {"[]MyType", "pkg", 2, true},
"[]*MyType": {"[]*MyType", "pkg", 3, true},
"...*MyType": {"[]*MyType", "pkg", 3, true},
var TypeExprs = map[string]model.TypeExpr{
"int": model.NewTypeExprFromData("int", "", 0, true),
"*int": model.NewTypeExprFromData("*int", "", 1, true),
"[]int": model.NewTypeExprFromData("[]int", "", 2, true),
"...int": model.NewTypeExprFromData("[]int", "", 2, true),
"[]*int": model.NewTypeExprFromData("[]*int", "", 3, true),
"...*int": model.NewTypeExprFromData("[]*int", "", 3, true),
"MyType": model.NewTypeExprFromData("MyType", "pkg", 0, true),
"*MyType": model.NewTypeExprFromData("*MyType", "pkg", 1, true),
"[]MyType": model.NewTypeExprFromData("[]MyType", "pkg", 2, true),
"...MyType": model.NewTypeExprFromData("[]MyType", "pkg", 2, true),
"[]*MyType": model.NewTypeExprFromData("[]*MyType", "pkg", 3, true),
"...*MyType": model.NewTypeExprFromData("[]*MyType", "pkg", 3, true),
"map[int]MyType": model.NewTypeExprFromData("map[int]MyType", "pkg", 8, true),
"map[int]*MyType": model.NewTypeExprFromData("map[int]*MyType", "pkg", 9, true),
}
func TestTypeExpr(t *testing.T) {
@@ -136,60 +137,9 @@ func TestTypeExpr(t *testing.T) {
expr = &ast.Ellipsis{Ellipsis: expr.Pos(), Elt: expr}
}
actual := NewTypeExpr("pkg", expr)
actual := model.NewTypeExprFromAst("pkg", expr)
if !reflect.DeepEqual(expected, actual) {
t.Error("Fail, expected", expected, ", was", actual)
}
}
}
func TestProcessBookingSource(t *testing.T) {
revel.Init("prod", "github.com/revel/examples/booking", "")
sourceInfo, err := ProcessSource([]string{revel.AppPath})
if err != nil {
t.Fatal("Failed to process booking source with error:", err)
}
controllerPackage := "github.com/revel/examples/booking/app/controllers"
expectedControllerSpecs := []*TypeInfo{
{"GorpController", controllerPackage, "controllers", nil, nil},
{"Application", controllerPackage, "controllers", nil, nil},
{"Hotels", controllerPackage, "controllers", nil, nil},
}
if len(sourceInfo.ControllerSpecs()) != len(expectedControllerSpecs) {
t.Errorf("Unexpected number of controllers found. Expected %d, Found %d",
len(expectedControllerSpecs), len(sourceInfo.ControllerSpecs()))
}
NEXT_TEST:
for _, expected := range expectedControllerSpecs {
for _, actual := range sourceInfo.ControllerSpecs() {
if actual.StructName == expected.StructName {
if actual.ImportPath != expected.ImportPath {
t.Errorf("%s expected to have import path %s, actual %s",
actual.StructName, expected.ImportPath, actual.ImportPath)
}
if actual.PackageName != expected.PackageName {
t.Errorf("%s expected to have package name %s, actual %s",
actual.StructName, expected.PackageName, actual.PackageName)
}
continue NEXT_TEST
}
}
t.Errorf("Expected to find controller %s, but did not. Actuals: %s",
expected.StructName, sourceInfo.ControllerSpecs())
}
}
func BenchmarkProcessBookingSource(b *testing.B) {
revel.Init("", "github.com/revel/examples/booking", "")
revel.TRACE = log.New(ioutil.Discard, "", 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := ProcessSource(revel.CodePaths)
if err != nil {
b.Error("Unexpected error:", err)
}
}
}

115
parser/validation.go Normal file
View File

@@ -0,0 +1,115 @@
package parser
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"go/ast"
"go/token"
)
// Scan app source code for calls to X.Y(), where X is of type *Validation.
//
// Recognize these scenarios:
// - "Y" = "Validation" and is a member of the receiver.
// (The common case for inline validation)
// - "X" is passed in to the func as a parameter.
// (For structs implementing Validated)
//
// The line number to which a validation call is attributed is that of the
// surrounding ExprStmt. This is so that it matches what runtime.Callers()
// reports.
//
// The end result is that we can set the default validation key for each call to
// be the same as the local variable.
func GetValidationKeys(fname string, fset *token.FileSet, funcDecl *ast.FuncDecl, imports map[string]string) map[int]string {
var (
lineKeys = make(map[int]string)
// Check the func parameters and the receiver's members for the *revel.Validation type.
validationParam = getValidationParameter(funcDecl, imports)
)
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// e.g. c.Validation.Required(arg) or v.Required(arg)
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// e.g. c.Validation.Required or v.Required
funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
switch x := funcSelector.X.(type) {
case *ast.SelectorExpr: // e.g. c.Validation
if x.Sel.Name != "Validation" {
return true
}
case *ast.Ident: // e.g. v
if validationParam == nil || x.Obj != validationParam {
return true
}
default:
return true
}
if len(callExpr.Args) == 0 {
return true
}
// Given the validation expression, extract the key.
key := callExpr.Args[0]
switch expr := key.(type) {
case *ast.BinaryExpr:
// If the argument is a binary expression, take the first expression.
// (e.g. c.Validation.Required(myName != ""))
key = expr.X
case *ast.UnaryExpr:
// If the argument is a unary expression, drill in.
// (e.g. c.Validation.Required(!myBool)
key = expr.X
case *ast.BasicLit:
// If it's a literal, skip it.
return true
}
if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid {
lineKeys[fset.Position(callExpr.End()).Line] = typeExpr.TypeName("")
} else {
utils.Logger.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", fname,
"line", fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String())
}
return true
})
return lineKeys
}
// Check to see if there is a *revel.Validation as an argument.
func getValidationParameter(funcDecl *ast.FuncDecl, imports map[string]string) *ast.Object {
for _, field := range funcDecl.Type.Params.List {
starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation
if !ok {
continue
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation
if !ok {
continue
}
xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev
if !ok {
continue
}
if selExpr.Sel.Name == "Validation" && imports[xIdent.Name] == model.RevelImportPath {
return field.Names[0].Obj
}
}
return nil
}

View File

@@ -0,0 +1,424 @@
package parser2
import (
"github.com/revel/cmd/utils"
"golang.org/x/tools/go/packages"
"github.com/revel/cmd/model"
"go/ast"
"go/token"
"strings"
"path/filepath"
"github.com/revel/cmd/logger"
)
type (
SourceInfoProcessor struct {
sourceProcessor *SourceProcessor
}
)
func NewSourceInfoProcessor(sourceProcessor *SourceProcessor) *SourceInfoProcessor {
return &SourceInfoProcessor{sourceProcessor:sourceProcessor}
}
func (s *SourceInfoProcessor) processPackage(p *packages.Package) (sourceInfo *model.SourceInfo) {
sourceInfo = &model.SourceInfo{
ValidationKeys: map[string]map[int]string{},
}
var (
isController = strings.HasSuffix(p.PkgPath, "/controllers") ||
strings.Contains(p.PkgPath, "/controllers/")
isTest = strings.HasSuffix(p.PkgPath, "/tests") ||
strings.Contains(p.PkgPath, "/tests/")
methodMap = map[string][]*model.MethodSpec{}
)
localImportMap := map[string]string{}
log := s.sourceProcessor.log.New("package", p.PkgPath)
log.Info("Processing package")
for _, tree := range p.Syntax {
for _, decl := range tree.Decls {
s.sourceProcessor.packageMap[p.PkgPath] = filepath.Dir(p.Fset.Position(decl.Pos()).Filename)
if !s.addImport(decl, p, localImportMap, log) {
continue
}
spec, found := s.getStructTypeDecl(decl, p.Fset)
//log.Info("Checking file","filename", p.Fset.Position(decl.Pos()).Filename,"found",found)
if found {
if isController || isTest {
controllerSpec := s.getControllerSpec(spec, p, localImportMap)
sourceInfo.StructSpecs = append(sourceInfo.StructSpecs, controllerSpec)
}
} else {
// Not a type definition, this could be a method for a controller try to extract that
// Func declaration?
funcDecl, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
// This could be a controller action endpoint, check and add if needed
if isController &&
funcDecl.Recv != nil && // Must have a receiver
funcDecl.Name.IsExported() && // be public
funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) == 1 {
// return one result
if m, receiver := s.getControllerFunc(funcDecl, p); m != nil {
methodMap[receiver] = append(methodMap[receiver], m)
log.Info("Added method map to ", "receiver", receiver, "method", m.Name)
}
}
// Check for validation
if lineKeyMap := s.getValidation(funcDecl, p); len(lineKeyMap) > 1 {
sourceInfo.ValidationKeys[p.PkgPath + "." + s.getFuncName(funcDecl)] = lineKeyMap
}
if funcDecl.Name.Name == "init" {
sourceInfo.InitImportPaths = append(sourceInfo.InitImportPaths, p.PkgPath)
}
}
}
}
// Add the method specs to the struct specs.
for _, spec := range sourceInfo.StructSpecs {
spec.MethodSpecs = methodMap[spec.StructName]
}
return
}
// Scan app source code for calls to X.Y(), where X is of type *Validation.
//
// Recognize these scenarios:
// - "Y" = "Validation" and is a member of the receiver.
// (The common case for inline validation)
// - "X" is passed in to the func as a parameter.
// (For structs implementing Validated)
//
// The line number to which a validation call is attributed is that of the
// surrounding ExprStmt. This is so that it matches what runtime.Callers()
// reports.
//
// The end result is that we can set the default validation key for each call to
// be the same as the local variable.
func (s *SourceInfoProcessor) getValidation(funcDecl *ast.FuncDecl, p *packages.Package) (map[int]string) {
var (
lineKeys = make(map[int]string)
// Check the func parameters and the receiver's members for the *revel.Validation type.
validationParam = s.getValidationParameter(funcDecl)
)
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// e.g. c.Validation.Required(arg) or v.Required(arg)
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// e.g. c.Validation.Required or v.Required
funcSelector, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
switch x := funcSelector.X.(type) {
case *ast.SelectorExpr: // e.g. c.Validation
if x.Sel.Name != "Validation" {
return true
}
case *ast.Ident: // e.g. v
if validationParam == nil || x.Obj != validationParam {
return true
}
default:
return true
}
if len(callExpr.Args) == 0 {
return true
}
// Given the validation expression, extract the key.
key := callExpr.Args[0]
switch expr := key.(type) {
case *ast.BinaryExpr:
// If the argument is a binary expression, take the first expression.
// (e.g. c.Validation.Required(myName != ""))
key = expr.X
case *ast.UnaryExpr:
// If the argument is a unary expression, drill in.
// (e.g. c.Validation.Required(!myBool)
key = expr.X
case *ast.BasicLit:
// If it's a literal, skip it.
return true
}
if typeExpr := model.NewTypeExprFromAst("", key); typeExpr.Valid {
lineKeys[p.Fset.Position(callExpr.End()).Line] = typeExpr.TypeName("")
} else {
s.sourceProcessor.log.Error("Error: Failed to generate key for field validation. Make sure the field name is valid.", "file", p.PkgPath,
"line", p.Fset.Position(callExpr.End()).Line, "function", funcDecl.Name.String())
}
return true
})
return lineKeys
}
// Check to see if there is a *revel.Validation as an argument.
func (s *SourceInfoProcessor) getValidationParameter(funcDecl *ast.FuncDecl) *ast.Object {
for _, field := range funcDecl.Type.Params.List {
starExpr, ok := field.Type.(*ast.StarExpr) // e.g. *revel.Validation
if !ok {
continue
}
selExpr, ok := starExpr.X.(*ast.SelectorExpr) // e.g. revel.Validation
if !ok {
continue
}
xIdent, ok := selExpr.X.(*ast.Ident) // e.g. rev
if !ok {
continue
}
if selExpr.Sel.Name == "Validation" && s.sourceProcessor.importMap[xIdent.Name] == model.RevelImportPath {
return field.Names[0].Obj
}
}
return nil
}
func (s *SourceInfoProcessor) getControllerFunc(funcDecl *ast.FuncDecl, p *packages.Package) (method *model.MethodSpec, recvTypeName string) {
selExpr, ok := funcDecl.Type.Results.List[0].Type.(*ast.SelectorExpr)
if !ok {
return
}
if selExpr.Sel.Name != "Result" {
return
}
if pkgIdent, ok := selExpr.X.(*ast.Ident); !ok || s.sourceProcessor.importMap[pkgIdent.Name] != model.RevelImportPath {
return
}
method = &model.MethodSpec{
Name: funcDecl.Name.Name,
}
// Add a description of the arguments to the method.
for _, field := range funcDecl.Type.Params.List {
for _, name := range field.Names {
var importPath string
typeExpr := model.NewTypeExprFromAst(p.Name, field.Type)
if !typeExpr.Valid {
utils.Logger.Warn("Warn: Didn't understand argument '%s' of action %s. Ignoring.", name, s.getFuncName(funcDecl))
return // We didn't understand one of the args. Ignore this action.
}
// Local object
if typeExpr.PkgName == p.Name {
importPath = p.PkgPath
} else if typeExpr.PkgName != "" {
var ok bool
if importPath, ok = s.sourceProcessor.importMap[typeExpr.PkgName]; !ok {
utils.Logger.Fatalf("Failed to find import for arg of type: %s , %s", typeExpr.PkgName, typeExpr.TypeName(""))
}
}
method.Args = append(method.Args, &model.MethodArg{
Name: name.Name,
TypeExpr: typeExpr,
ImportPath: importPath,
})
}
}
// Add a description of the calls to Render from the method.
// Inspect every node (e.g. always return true).
method.RenderCalls = []*model.MethodCall{}
ast.Inspect(funcDecl.Body, func(node ast.Node) bool {
// Is it a function call?
callExpr, ok := node.(*ast.CallExpr)
if !ok {
return true
}
// Is it calling (*Controller).Render?
selExpr, ok := callExpr.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// The type of the receiver is not easily available, so just store every
// call to any method called Render.
if selExpr.Sel.Name != "Render" {
return true
}
// Add this call's args to the renderArgs.
pos := p.Fset.Position(callExpr.Lparen)
methodCall := &model.MethodCall{
Line: pos.Line,
Names: []string{},
}
for _, arg := range callExpr.Args {
argIdent, ok := arg.(*ast.Ident)
if !ok {
continue
}
methodCall.Names = append(methodCall.Names, argIdent.Name)
}
method.RenderCalls = append(method.RenderCalls, methodCall)
return true
})
var recvType = funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
recvTypeName = recvStarType.X.(*ast.Ident).Name
} else {
recvTypeName = recvType.(*ast.Ident).Name
}
return
}
func (s *SourceInfoProcessor) getControllerSpec(spec *ast.TypeSpec, p *packages.Package, localImportMap map[string]string) (controllerSpec *model.TypeInfo) {
structType := spec.Type.(*ast.StructType)
// At this point we know it's a type declaration for a struct.
// Fill in the rest of the info by diving into the fields.
// Add it provisionally to the Controller list -- it's later filtered using field info.
controllerSpec = &model.TypeInfo{
StructName: spec.Name.Name,
ImportPath: p.PkgPath,
PackageName: p.Name,
}
log := s.sourceProcessor.log.New("file", p.Fset.Position(spec.Pos()).Filename, "position", p.Fset.Position(spec.Pos()).Line)
for _, field := range structType.Fields.List {
// If field.Names is set, it's not an embedded type.
if field.Names != nil {
continue
}
// A direct "sub-type" has an ast.Field as either:
// Ident { "AppController" }
// SelectorExpr { "rev", "Controller" }
// Additionally, that can be wrapped by StarExprs.
fieldType := field.Type
pkgName, typeName := func() (string, string) {
// Drill through any StarExprs.
for {
if starExpr, ok := fieldType.(*ast.StarExpr); ok {
fieldType = starExpr.X
continue
}
break
}
// If the embedded type is in the same package, it's an Ident.
if ident, ok := fieldType.(*ast.Ident); ok {
return "", ident.Name
}
if selectorExpr, ok := fieldType.(*ast.SelectorExpr); ok {
if pkgIdent, ok := selectorExpr.X.(*ast.Ident); ok {
return pkgIdent.Name, selectorExpr.Sel.Name
}
}
return "", ""
}()
// If a typename wasn't found, skip it.
if typeName == "" {
continue
}
// Find the import path for this type.
// If it was referenced without a package name, use the current package import path.
// Else, look up the package's import path by name.
var importPath string
if pkgName == "" {
importPath = p.PkgPath
} else {
var ok bool
if importPath, ok = localImportMap[pkgName]; !ok {
log.Debug("Debug: Unusual, failed to find package locally ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", )
if importPath, ok = s.sourceProcessor.importMap[pkgName]; !ok {
log.Error("Error: Failed to find import path for ", "package", pkgName, "type", typeName, "map", s.sourceProcessor.importMap, "usedin", )
continue
}
}
}
controllerSpec.EmbeddedTypes = append(controllerSpec.EmbeddedTypes, &model.EmbeddedTypeName{
ImportPath: importPath,
StructName: typeName,
})
}
s.sourceProcessor.log.Info("Added controller spec", "name", controllerSpec.StructName, "package", controllerSpec.ImportPath)
return
}
func (s *SourceInfoProcessor) getStructTypeDecl(decl ast.Decl, fset *token.FileSet) (spec *ast.TypeSpec, found bool) {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok != token.TYPE {
return
}
if len(genDecl.Specs) == 0 {
utils.Logger.Warn("Warn: Surprising: %s:%d Decl contains no specifications", fset.Position(decl.Pos()).Filename, fset.Position(decl.Pos()).Line)
return
}
spec = genDecl.Specs[0].(*ast.TypeSpec)
_, found = spec.Type.(*ast.StructType)
return
}
func (s *SourceInfoProcessor) getFuncName(funcDecl *ast.FuncDecl) string {
prefix := ""
if funcDecl.Recv != nil {
recvType := funcDecl.Recv.List[0].Type
if recvStarType, ok := recvType.(*ast.StarExpr); ok {
prefix = "(*" + recvStarType.X.(*ast.Ident).Name + ")"
} else {
prefix = recvType.(*ast.Ident).Name
}
prefix += "."
}
return prefix + funcDecl.Name.Name
}
func (s *SourceInfoProcessor) addImport(decl ast.Decl, p *packages.Package, localImportMap map[string]string, log logger.MultiLogger) (shouldContinue bool) {
shouldContinue = true
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
return
}
if genDecl.Tok == token.IMPORT {
shouldContinue = false
for _, spec := range genDecl.Specs {
importSpec := spec.(*ast.ImportSpec)
//fmt.Printf("*** import specification %#v\n", importSpec)
var pkgAlias string
if importSpec.Name != nil {
pkgAlias = importSpec.Name.Name
if pkgAlias == "_" {
continue
}
}
quotedPath := importSpec.Path.Value // e.g. "\"sample/app/models\""
fullPath := quotedPath[1 : len(quotedPath) - 1] // Remove the quotes
if pkgAlias == "" {
pkgAlias = fullPath
if index := strings.LastIndex(pkgAlias, "/"); index > 0 {
pkgAlias = pkgAlias[index + 1:]
}
}
localImportMap[pkgAlias] = fullPath
}
}
return
}

250
parser2/source_processor.go Normal file
View File

@@ -0,0 +1,250 @@
package parser2
import (
"github.com/revel/cmd/model"
"golang.org/x/tools/go/packages"
"github.com/revel/cmd/utils"
"go/parser"
"strings"
"github.com/revel/cmd/logger"
"os"
"path/filepath"
"go/ast"
"go/token"
"go/scanner"
)
type (
SourceProcessor struct {
revelContainer *model.RevelContainer
log logger.MultiLogger
packageList []*packages.Package
importMap map[string]string
packageMap map[string]string
sourceInfoProcessor *SourceInfoProcessor
sourceInfo *model.SourceInfo
}
)
func ProcessSource(revelContainer *model.RevelContainer) (sourceInfo *model.SourceInfo, compileError error) {
utils.Logger.Info("ProcessSource")
processor := NewSourceProcessor(revelContainer)
compileError = processor.parse()
sourceInfo = processor.sourceInfo
if compileError == nil {
processor.log.Infof("From parsers : Structures:%d InitImports:%d ValidationKeys:%d %v", len(sourceInfo.StructSpecs), len(sourceInfo.InitImportPaths), len(sourceInfo.ValidationKeys), sourceInfo.PackageMap)
}
return
}
func NewSourceProcessor(revelContainer *model.RevelContainer) *SourceProcessor {
s := &SourceProcessor{revelContainer:revelContainer, log:utils.Logger.New("parser", "SourceProcessor")}
s.sourceInfoProcessor = NewSourceInfoProcessor(s)
return s
}
func (s *SourceProcessor) parse() (compileError error) {
print("Parsing packages, (may require download if not cached)...")
if compileError = s.addPackages(); compileError != nil {
return
}
println(" Completed")
if compileError = s.addImportMap(); compileError != nil {
return
}
if compileError = s.addSourceInfo(); compileError != nil {
return
}
s.sourceInfo.PackageMap = map[string]string{}
getImportFromMap := func(packagePath string) string {
for path := range s.packageMap {
if strings.Index(path, packagePath) == 0 {
fullPath := s.packageMap[path]
return fullPath[:(len(fullPath) - len(path) + len(packagePath))]
}
}
return ""
}
s.sourceInfo.PackageMap[model.RevelImportPath] = getImportFromMap(model.RevelImportPath)
s.sourceInfo.PackageMap[s.revelContainer.ImportPath] = getImportFromMap(s.revelContainer.ImportPath)
for _, module := range s.revelContainer.ModulePathMap {
s.sourceInfo.PackageMap[module.ImportPath] = getImportFromMap(module.ImportPath)
}
return
}
// Using the packages.Load function load all the packages and type specifications (forces compile).
// this sets the SourceProcessor.packageList []*packages.Package
func (s *SourceProcessor) addPackages() (err error) {
allPackages := []string{model.RevelImportPath + "/..."}
for _, module := range s.revelContainer.ModulePathMap {
allPackages = append(allPackages, module.ImportPath + "/...") // +"/app/controllers/...")
}
s.log.Info("Reading packages", "packageList", allPackages)
//allPackages = []string{s.revelContainer.ImportPath + "/..."} //+"/app/controllers/..."}
config := &packages.Config{
// ode: packages.NeedSyntax | packages.NeedCompiledGoFiles,
Mode:
packages.NeedTypes | // For compile error
packages.NeedDeps | // To load dependent files
packages.NeedName | // Loads the full package name
packages.NeedSyntax, // To load ast tree (for end points)
//Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles |
// packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile |
// packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo |
// packages.NeedTypesSizes,
//Mode: packages.NeedName | packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | packages.NeedFiles |
// packages.NeedCompiledGoFiles | packages.NeedTypesSizes |
// packages.NeedSyntax | packages.NeedCompiledGoFiles ,
//Mode: packages.NeedSyntax | packages.NeedCompiledGoFiles | packages.NeedName | packages.NeedFiles |
// packages.LoadTypes | packages.NeedTypes | packages.NeedDeps, //, // |
// packages.NeedTypes, // packages.LoadTypes | packages.NeedSyntax | packages.NeedTypesInfo,
//packages.LoadSyntax | packages.NeedDeps,
Dir:s.revelContainer.AppPath,
}
s.packageList, err = packages.Load(config, allPackages...)
s.log.Info("Loaded modules ", "len results", len(s.packageList), "error", err)
// Now process the files in the aap source folder s.revelContainer.ImportPath + "/...",
err = utils.Walk(s.revelContainer.AppPath, s.processPath)
s.log.Info("Loaded apps and modules ", "len results", len(s.packageList), "error", err)
return
}
// This callback is used to build the packages for the "app" package. This allows us to
// parse the source files without doing a full compile on them
// This callback only processes folders, so any files passed to this will return a nil
func (s *SourceProcessor) processPath(path string, info os.FileInfo, err error) error {
if err != nil {
s.log.Error("Error scanning app source:", "error", err)
return nil
}
// Ignore files and folders not marked tmp (since those are generated)
if !info.IsDir() || info.Name() == "tmp" {
return nil
}
// Real work for processing the folder
pkgImportPath := s.revelContainer.ImportPath
appPath := s.revelContainer.BasePath
if appPath != path {
pkgImportPath = s.revelContainer.ImportPath + "/" + filepath.ToSlash(path[len(appPath)+1:])
}
// Parse files within the path.
var pkgMap map[string]*ast.Package
fset := token.NewFileSet()
pkgMap, err = parser.ParseDir(
fset,
path,
func(f os.FileInfo) bool {
return !f.IsDir() && !strings.HasPrefix(f.Name(), ".") && strings.HasSuffix(f.Name(), ".go")
},
0)
if err != nil {
if errList, ok := err.(scanner.ErrorList); ok {
var pos = errList[0].Pos
newError := &utils.SourceError{
SourceType: ".go source",
Title: "Go Compilation Error",
Path: pos.Filename,
Description: errList[0].Msg,
Line: pos.Line,
Column: pos.Column,
SourceLines: utils.MustReadLines(pos.Filename),
}
errorLink := s.revelContainer.Config.StringDefault("error.link", "")
if errorLink != "" {
newError.SetLink(errorLink)
}
return newError
}
// This is exception, err already checked above. Here just a print
ast.Print(nil, err)
s.log.Fatal("Failed to parse dir", "error", err)
}
// Skip "main" packages.
delete(pkgMap, "main")
// Ignore packages that end with _test
// These cannot be included in source code that is not generated specifically as a test
for i := range pkgMap {
if len(i) > 6 {
if string(i[len(i)-5:]) == "_test" {
delete(pkgMap, i)
}
}
}
// If there is no code in this directory, skip it.
if len(pkgMap) == 0 {
return nil
}
// There should be only one package in this directory.
if len(pkgMap) > 1 {
for i := range pkgMap {
println("Found package ", i)
}
utils.Logger.Fatal("Most unexpected! Multiple packages in a single directory:", "packages", pkgMap)
}
// At this point there is only one package in the pkgs map,
p := &packages.Package{}
p.PkgPath = pkgImportPath
p.Fset = fset
for _, pkg := range pkgMap {
p.Name = pkg.Name
s.log.Info("Found package","pkg.Name", pkg.Name,"p.Name", p.PkgPath)
for filename,astFile := range pkg.Files {
p.Syntax = append(p.Syntax,astFile)
p.GoFiles = append(p.GoFiles,filename)
}
}
s.packageList = append(s.packageList, p)
return nil
}
// This function is used to populate a map so that we can lookup controller embedded types in order to determine
// if a Struct inherits from from revel.Controller
func (s *SourceProcessor) addImportMap() (err error) {
s.importMap = map[string]string{}
s.packageMap = map[string]string{}
for _, p := range s.packageList {
if len(p.Errors) > 0 {
// Generate a compile error
for _, e := range p.Errors {
s.log.Info("While reading packages encountered import error ignoring ", "PkgPath", p.PkgPath, "error", e)
}
}
for _, tree := range p.Syntax {
s.importMap[tree.Name.Name] = p.PkgPath
}
}
return
}
func (s *SourceProcessor) addSourceInfo() (err error) {
for _, p := range s.packageList {
if sourceInfo := s.sourceInfoProcessor.processPackage(p); sourceInfo != nil {
if s.sourceInfo != nil {
s.sourceInfo.Merge(sourceInfo)
} else {
s.sourceInfo = sourceInfo
}
}
}
return
}

13
proxy/proxy.go Normal file
View File

@@ -0,0 +1,13 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// Package proxy for a Revel Framework.
//
// It has a following responsibilities:
// 1. Build and run the users program in a proxy
// 2. Monitor the user source and restart the program when necessary.
//
// Source files are generated in the app/tmp directory.
package proxy

View File

@@ -5,91 +5,169 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"fmt"
"github.com/revel/cmd/harness"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
)
var cmdBuild = &Command{
UsageLine: "build [import path] [target path] [run mode]",
UsageLine: "revel build [-r [run mode]] [import path] [target path] ",
Short: "build a Revel application (e.g. for deployment)",
Long: `
Build the Revel web application named by the given import path.
This allows it to be deployed and run on a machine that lacks a Go installation.
The run mode is used to select which set of app.conf configuration should
apply and may be used to determine logic in the application itself.
Run mode defaults to "dev".
WARNING: The target path will be completely deleted, if it already exists!
For example:
revel build github.com/revel/examples/chat /tmp/chat
`,
}
func init() {
cmdBuild.Run = buildApp
cmdBuild.RunWith = buildApp
cmdBuild.UpdateConfig = updateBuildConfig
}
func buildApp(args []string) {
// The update config updates the configuration command so that it can run
func updateBuildConfig(c *model.CommandConfig, args []string) bool {
c.Index = model.BUILD
if c.Build.TargetPath == "" {
c.Build.TargetPath = "target"
}
if len(args) == 0 && c.Build.ImportPath != "" {
return true
}
// If arguments were passed in then there must be two
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "%s\n%s", cmdBuild.UsageLine, cmdBuild.Long)
return false
}
c.Build.ImportPath = args[0]
c.Build.TargetPath = args[1]
if len(args) > 2 {
c.Build.Mode = args[2]
}
return true
}
// The main entry point to build application from command line
func buildApp(c *model.CommandConfig) (err error) {
appImportPath, destPath, mode := c.ImportPath, c.Build.TargetPath, DefaultRunMode
if len(c.Build.Mode) > 0 {
mode = c.Build.Mode
}
// Convert target to absolute path
c.Build.TargetPath, _ = filepath.Abs(destPath)
c.Build.Mode = mode
c.Build.ImportPath = appImportPath
revel_paths, err := model.NewRevelPaths(mode, appImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
if err != nil {
return
}
appImportPath, destPath, mode := args[0], args[1], DefaultRunMode
if len(args) >= 3 {
mode = args[2]
if err = buildSafetyCheck(destPath); err != nil {
return
}
if !revel.Initialized {
revel.Init(mode, appImportPath, "")
// Ensure the application can be built, this generates the main file
app, err := harness.Build(c, revel_paths)
if err != nil {
return err
}
// First, verify that it is either already empty or looks like a previous
// build (to avoid clobbering anything)
if exists(destPath) && !empty(destPath) && !exists(filepath.Join(destPath, "run.sh")) {
errorf("Abort: %s exists and does not look like a build directory.", destPath)
}
if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) {
revel.ERROR.Fatalln(err)
}
if err := os.MkdirAll(destPath, 0777); err != nil {
revel.ERROR.Fatalln(err)
}
app, reverr := harness.Build()
panicOnError(reverr, "Failed to build")
// Copy files
// Included are:
// - run scripts
// - binary
// - revel
// - app
packageFolders, err := buildCopyFiles(c, app, revel_paths)
if err != nil {
return
}
err = buildCopyModules(c, revel_paths, packageFolders, app)
if err != nil {
return
}
err = buildWriteScripts(c, app)
if err != nil {
return
}
return
}
// Copy the files to the target
func buildCopyFiles(c *model.CommandConfig, app *harness.App, revel_paths *model.RevelContainer) (packageFolders []string, err error) {
appImportPath, destPath := c.ImportPath, c.Build.TargetPath
// Revel and the app are in a directory structure mirroring import path
srcPath := filepath.Join(destPath, "src")
destBinaryPath := filepath.Join(destPath, filepath.Base(app.BinaryPath))
tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(revel.RevelImportPath))
mustCopyFile(destBinaryPath, app.BinaryPath)
mustChmod(destBinaryPath, 0755)
_ = mustCopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel.RevelPath, "conf"), nil)
_ = mustCopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel.RevelPath, "templates"), nil)
_ = mustCopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel.BasePath, nil)
tmpRevelPath := filepath.Join(srcPath, filepath.FromSlash(model.RevelImportPath))
if err = utils.CopyFile(destBinaryPath, filepath.Join(revel_paths.BasePath, app.BinaryPath)); err != nil {
return
}
utils.MustChmod(destBinaryPath, 0755)
// Copy the templates from the revel
if err = utils.CopyDir(filepath.Join(tmpRevelPath, "conf"), filepath.Join(revel_paths.RevelPath, "conf"), nil); err != nil {
return
}
if err = utils.CopyDir(filepath.Join(tmpRevelPath, "templates"), filepath.Join(revel_paths.RevelPath, "templates"), nil); err != nil {
return
}
// Get the folders to be packaged
packageFolders = strings.Split(revel_paths.Config.StringDefault("package.folders", "conf,public,app/views"), ",")
for i, p := range packageFolders {
// Clean spaces, reformat slash to filesystem
packageFolders[i] = filepath.FromSlash(strings.TrimSpace(p))
}
if c.Build.CopySource {
err = utils.CopyDir(filepath.Join(srcPath, filepath.FromSlash(appImportPath)), revel_paths.BasePath, nil)
if err != nil {
return
}
} else {
for _, folder := range packageFolders {
err = utils.CopyDir(
filepath.Join(srcPath, filepath.FromSlash(appImportPath), folder),
filepath.Join(revel_paths.BasePath, folder),
nil)
if err != nil {
return
}
}
}
return
}
// Based on the section copy over the build modules
func buildCopyModules(c *model.CommandConfig, revel_paths *model.RevelContainer, packageFolders []string, app *harness.App) (err error) {
destPath := filepath.Join(c.Build.TargetPath, "src")
// Find all the modules used and copy them over.
config := revel.Config.Raw()
modulePaths := make(map[string]string) // import path => filesystem path
config := revel_paths.Config.Raw()
// We should only copy over the section of options what the build is targeted for
// We will default to prod
moduleImportList := []string{}
for _, section := range config.Sections() {
// If the runmode is defined we will only import modules defined for that run mode
if c.Build.Mode != "" && c.Build.Mode != section {
continue
}
options, _ := config.SectionOptions(section)
for _, key := range options {
if !strings.HasPrefix(key, "module.") {
@@ -99,32 +177,92 @@ func buildApp(args []string) {
if moduleImportPath == "" {
continue
}
modulePath, err := revel.ResolveImportPath(moduleImportPath)
if err != nil {
revel.ERROR.Fatalln("Failed to load module %s: %s", key[len("module."):], err)
}
modulePaths[moduleImportPath] = modulePath
moduleImportList = append(moduleImportList, moduleImportPath)
}
}
for importPath, fsPath := range modulePaths {
_ = mustCopyDir(filepath.Join(srcPath, importPath), fsPath, nil)
// Copy the the paths for each of the modules
for _, importPath := range moduleImportList {
fsPath := app.PackagePathMap[importPath]
utils.Logger.Info("Copy files ", "to", filepath.Join(destPath, importPath), "from", fsPath)
if c.Build.CopySource {
err = utils.CopyDir(filepath.Join(destPath, importPath), fsPath, nil)
if err != nil {
return
}
} else {
for _, folder := range packageFolders {
err = utils.CopyDir(
filepath.Join(destPath, importPath, folder),
filepath.Join(fsPath, folder),
nil)
if err != nil {
return
}
}
}
}
tmplData, runShPath := map[string]interface{}{
"BinName": filepath.Base(app.BinaryPath),
"ImportPath": appImportPath,
"Mode": mode,
}, filepath.Join(destPath, "run.sh")
mustRenderTemplate(
runShPath,
filepath.Join(revel.RevelPath, "..", "cmd", "revel", "package_run.sh.template"),
tmplData)
mustChmod(runShPath, 0755)
mustRenderTemplate(
filepath.Join(destPath, "run.bat"),
filepath.Join(revel.RevelPath, "..", "cmd", "revel", "package_run.bat.template"),
tmplData)
return
}
// Write the run scripts for the build
func buildWriteScripts(c *model.CommandConfig, app *harness.App) (err error) {
tmplData := map[string]interface{}{
"BinName": filepath.Base(app.BinaryPath),
"ImportPath": c.Build.ImportPath,
"Mode": c.Build.Mode,
}
err = utils.GenerateTemplate(
filepath.Join(c.Build.TargetPath, "run.sh"),
PACKAGE_RUN_SH,
tmplData,
)
if err != nil {
return
}
utils.MustChmod(filepath.Join(c.Build.TargetPath, "run.sh"), 0755)
err = utils.GenerateTemplate(
filepath.Join(c.Build.TargetPath, "run.bat"),
PACKAGE_RUN_BAT,
tmplData,
)
if err != nil {
return
}
fmt.Println("Your application has been built in:", c.Build.TargetPath)
return
}
// Checks to see if the target folder exists and can be created
func buildSafetyCheck(destPath string) error {
// First, verify that it is either already empty or looks like a previous
// build (to avoid clobbering anything)
if utils.Exists(destPath) && !utils.Empty(destPath) && !utils.Exists(filepath.Join(destPath, "run.sh")) {
return utils.NewBuildError("Abort: %s exists and does not look like a build directory.", "path", destPath)
}
if err := os.RemoveAll(destPath); err != nil && !os.IsNotExist(err) {
return utils.NewBuildIfError(err, "Remove all error", "path", destPath)
}
if err := os.MkdirAll(destPath, 0777); err != nil {
return utils.NewBuildIfError(err, "MkDir all error", "path", destPath)
}
return nil
}
const PACKAGE_RUN_SH = `#!/bin/sh
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode {{.Mode}}
`
const PACKAGE_RUN_BAT = `@echo off
{{.BinName}} -importPath {{.ImportPath}} -srcPath "%CD%\src" -runMode {{.Mode}}
`

34
revel/build_test.go Normal file
View File

@@ -0,0 +1,34 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/revel/cmd/utils"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
// test the commands
func TestBuild(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-build", a)
t.Run("Build", func(t *testing.T) {
a := assert.New(t)
c := newApp("build-test", model.NEW, nil, a)
main.Commands[model.NEW].RunWith(c)
c.Index = model.BUILD
c.Build.TargetPath = filepath.Join(gopath, "build-test", "target")
c.Build.ImportPath = c.ImportPath
a.Nil(main.Commands[model.BUILD].RunWith(c), "Failed to run build-test")
a.True(utils.Exists(filepath.Join(gopath, "build-test", "target")))
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

View File

@@ -6,7 +6,9 @@ package main
import (
"fmt"
"go/build"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"os"
"path/filepath"
)
@@ -22,36 +24,45 @@ For example:
revel clean github.com/revel/examples/chat
It removes the app/tmp and app/routes directory.
`,
}
func init() {
cmdClean.Run = cleanApp
cmdClean.UpdateConfig = updateCleanConfig
cmdClean.RunWith = cleanApp
}
func cleanApp(args []string) {
// Update the clean command configuration, using old method
func updateCleanConfig(c *model.CommandConfig, args []string) bool {
c.Index = model.CLEAN
if len(args) == 0 && c.Clean.ImportPath != "" {
return true
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, cmdClean.Long)
return
return false
}
c.Clean.ImportPath = args[0]
return true
}
appPkg, err := build.Import(args[0], "", build.FindOnly)
if err != nil {
fmt.Fprintln(os.Stderr, "Abort: Failed to find import path:", err)
return
}
// Clean the source directory of generated files
func cleanApp(c *model.CommandConfig) (err error) {
purgeDirs := []string{
filepath.Join(appPkg.Dir, "app", "tmp"),
filepath.Join(appPkg.Dir, "app", "routes"),
filepath.Join(c.AppPath, "app", "tmp"),
filepath.Join(c.AppPath, "app", "routes"),
}
for _, dir := range purgeDirs {
fmt.Println("Removing:", dir)
err = os.RemoveAll(dir)
if err != nil {
fmt.Fprintln(os.Stderr, "Abort:", err)
utils.Logger.Error("Failed to clean dir", "error", err)
return
}
}
return err
}

37
revel/clean_test.go Normal file
View File

@@ -0,0 +1,37 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/revel/cmd/utils"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
// test the commands
func TestClean(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-clean", a)
t.Run("Clean", func(t *testing.T) {
a := assert.New(t)
c := newApp("clean-test", model.NEW, nil, a)
main.Commands[model.NEW].RunWith(c)
c.Index = model.TEST
main.Commands[model.TEST].RunWith(c)
a.True(utils.Exists(filepath.Join(gopath, "clean-test", "app", "tmp", "main.go")),
"Missing main from path "+filepath.Join(gopath, "clean-test", "app", "tmp", "main.go"))
c.Clean.ImportPath = c.ImportPath
a.Nil(main.Commands[model.CLEAN].RunWith(c), "Failed to run clean-test")
a.False(utils.Exists(filepath.Join(gopath, "clean-test", "app", "tmp", "main.go")),
"Did not remove main from path "+filepath.Join(gopath, "clean-test", "app", "tmp", "main.go"))
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

92
revel/command_test.go Normal file
View File

@@ -0,0 +1,92 @@
package main_test
import (
"github.com/revel/cmd/logger"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"github.com/stretchr/testify/assert"
"go/build"
"os"
"os/exec"
"path/filepath"
"fmt"
)
// Test that the event handler can be attached and it dispatches the event received
func setup(suffix string, a *assert.Assertions) (string) {
temp := os.TempDir()
wd, _ := os.Getwd()
utils.InitLogger(wd, logger.LvlInfo)
gopath := filepath.Join(temp, "revel-test",suffix)
if utils.Exists(gopath) {
utils.Logger.Info("Removing test path", "path", gopath)
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
err := os.MkdirAll(gopath, os.ModePerm)
a.Nil(err, "Failed to create gopath "+gopath)
// So this is the issue, on the mac when folders are created in a temp folder they are returned like
// /var/folders/nz/vv4_9tw56nv9k3tkvyszvwg80000gn/T/revel-test/revel-test-build
// But if you change into that directory and read the current folder it is
// /private/var/folders/nz/vv4_9tw56nv9k3tkvyszvwg80000gn/T/revel-test/revel-test-build
// So to make this work on darwin this code was added
os.Chdir(gopath)
newwd, _ := os.Getwd()
gopath = newwd
defaultBuild := build.Default
defaultBuild.GOPATH = gopath
build.Default = defaultBuild
utils.Logger.Info("Setup stats", "original wd", wd, "new wd", newwd, "gopath",gopath, "gopath exists", utils.DirExists(gopath), "wd exists", utils.DirExists(newwd))
return gopath
}
// Create a new app for the name
func newApp(name string, command model.COMMAND, precall func(c *model.CommandConfig), a *assert.Assertions) *model.CommandConfig {
c := &model.CommandConfig{Vendored:true}
switch command {
case model.NEW:
c.New.ImportPath = name
c.New.Callback=func() error {
// On callback we will invoke a specific branch of revel so that it works
goModCmd := exec.Command("go", "mod", "tidy")
utils.CmdInit(goModCmd, !c.Vendored, c.AppPath)
getOutput, _ := goModCmd.CombinedOutput()
fmt.Printf("Calling go mod tidy %s",string(getOutput))
goModCmd = exec.Command("go", "mod", "edit", "-replace=github.com/revel/revel=github.com/revel/revel@develop")
utils.CmdInit(goModCmd, !c.Vendored, c.AppPath)
getOutput, _ = goModCmd.CombinedOutput()
fmt.Printf("Calling go mod edit %v",string(getOutput))
return nil
}
case model.BUILD:
c.Build.ImportPath = name
case model.TEST:
c.Test.ImportPath = name
case model.PACKAGE:
c.Package.ImportPath = name
case model.VERSION:
c.Version.ImportPath = name
case model.CLEAN:
c.Clean.ImportPath = name
default:
a.Fail("Unknown command ", command)
}
c.Index = command
if precall != nil {
precall(c)
}
if c.UpdateImportPath()!=nil {
a.Fail("Unable to update import path")
}
c.InitPackageResolver()
return c
}

View File

@@ -5,21 +5,21 @@
package main
import (
"bytes"
"fmt"
"go/build"
"log"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"net/url"
)
var cmdNew = &Command{
UsageLine: "new [path] [skeleton]",
UsageLine: "new -i [path] -s [skeleton]",
Short: "create a skeleton Revel application",
Long: `
New creates a few files to get a new Revel application running quickly.
@@ -31,63 +31,168 @@ Skeleton is an optional argument, provided as an import path
For example:
revel new import/path/helloworld
revel new -a import/path/helloworld
revel new -a import/path/helloworld -s import/path/skeleton
revel new import/path/helloworld import/path/skeleton
`,
}
func init() {
cmdNew.Run = newApp
cmdNew.RunWith = newApp
cmdNew.UpdateConfig = updateNewConfig
}
var (
// Called when unable to parse the command line automatically and assumes an old launch
func updateNewConfig(c *model.CommandConfig, args []string) bool {
c.Index = model.NEW
if len(c.New.Package) > 0 {
c.New.NotVendored = false
}
c.Vendored = !c.New.NotVendored
// go related paths
gopath string
gocmd string
srcRoot string
// revel related paths
revelPkg *build.Package
revelCmdPkg *build.Package
appPath string
appName string
basePath string
importPath string
skeletonPath string
)
func newApp(args []string) {
// check for proper args by count
if len(args) == 0 {
errorf("No import path given.\nRun 'revel help new' for usage.\n")
if len(c.New.ImportPath) == 0 {
fmt.Fprintf(os.Stderr, cmdNew.Long)
return false
}
return true
}
if len(args) > 2 {
errorf("Too many arguments provided.\nRun 'revel help new' for usage.\n")
c.New.ImportPath = args[0]
if len(args) > 1 {
c.New.SkeletonPath = args[1]
}
revel.ERROR.SetFlags(log.LstdFlags)
return true
// checking and setting go paths
initGoPaths()
}
// checking and setting application
setApplicationPath(args)
// Call to create a new application
func newApp(c *model.CommandConfig) (err error) {
// Check for an existing folder so we don't clobber it
_, err = build.Import(c.ImportPath, "", build.FindOnly)
if err == nil || !utils.Empty(c.AppPath) {
return utils.NewBuildError("Abort: Import path already exists.", "path", c.ImportPath, "apppath", c.AppPath)
}
// checking and setting skeleton
setSkeletonPath(args)
if err = setSkeletonPath(c); err != nil {
return
}
// Create application path
if err := os.MkdirAll(c.AppPath, os.ModePerm); err != nil {
return utils.NewBuildError("Abort: Unable to create app path.", "path", c.AppPath)
}
// checking and setting application
if err = setApplicationPath(c); err != nil {
return err
}
// This kicked off the download of the revel app, not needed for vendor
if !c.Vendored {
// At this point the versions can be set
c.SetVersions()
}
// copy files to new app directory
copyNewAppFiles()
if err = copyNewAppFiles(c); err != nil {
return
}
// Run the vendor tool if needed
if c.Vendored {
if err = createModVendor(c); err != nil {
return
}
}
// goodbye world
fmt.Fprintln(os.Stdout, "Your application is ready:\n ", appPath)
fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run", importPath)
fmt.Fprintln(os.Stdout, "Your application has been created in:\n ", c.AppPath)
// Check to see if it should be run right off
if c.New.Run {
// Need to prep the run command
c.Run.ImportPath = c.ImportPath
updateRunConfig(c,nil)
c.UpdateImportPath()
runApp(c)
} else {
fmt.Fprintln(os.Stdout, "\nYou can run it with:\n revel run -a ", c.ImportPath)
}
return
}
func createModVendor(c *model.CommandConfig) (err error) {
utils.Logger.Info("Creating a new mod app")
goModCmd := exec.Command("go", "mod", "init", filepath.Join(c.New.Package, c.AppName))
utils.CmdInit(goModCmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec:", "args", goModCmd.Args, "env", goModCmd.Env, "workingdir", goModCmd.Dir)
getOutput, err := goModCmd.CombinedOutput()
if c.New.Callback != nil {
err = c.New.Callback()
}
if err != nil {
return utils.NewBuildIfError(err, string(getOutput))
}
return
}
func createDepVendor(c *model.CommandConfig) (err error) {
utils.Logger.Info("Creating a new vendor app")
vendorPath := filepath.Join(c.AppPath, "vendor")
if !utils.DirExists(vendorPath) {
if err := os.MkdirAll(vendorPath, os.ModePerm); err != nil {
return utils.NewBuildError("Failed to create " + vendorPath, "error", err)
}
}
// In order for dep to run there needs to be a source file in the folder
tempPath := filepath.Join(c.AppPath, "tmp")
utils.Logger.Info("Checking for temp folder for source code", "path", tempPath)
if !utils.DirExists(tempPath) {
if err := os.MkdirAll(tempPath, os.ModePerm); err != nil {
return utils.NewBuildIfError(err, "Failed to create " + vendorPath)
}
if err = utils.GenerateTemplate(filepath.Join(tempPath, "main.go"), NEW_MAIN_FILE, nil); err != nil {
return utils.NewBuildIfError(err, "Failed to create main file " + vendorPath)
}
}
// Create a package template file if it does not exist
packageFile := filepath.Join(c.AppPath, "Gopkg.toml")
utils.Logger.Info("Checking for Gopkg.toml", "path", packageFile)
if !utils.Exists(packageFile) {
utils.Logger.Info("Generating Gopkg.toml", "path", packageFile)
if err := utils.GenerateTemplate(packageFile, VENDOR_GOPKG, nil); err != nil {
return utils.NewBuildIfError(err, "Failed to generate template")
}
} else {
utils.Logger.Info("Package file exists in skeleto, skipping adding")
}
getCmd := exec.Command("dep", "ensure", "-v")
utils.CmdInit(getCmd, !c.Vendored, c.AppPath)
utils.Logger.Info("Exec:", "args", getCmd.Args, "env", getCmd.Env, "workingdir", getCmd.Dir)
getOutput, err := getCmd.CombinedOutput()
if err != nil {
return utils.NewBuildIfError(err, string(getOutput))
}
return
}
// Used to generate a new secret key
const alphaNumeric = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// Generate a secret key
func generateSecret() string {
chars := make([]byte, 64)
for i := 0; i < 64; i++ {
@@ -96,128 +201,190 @@ func generateSecret() string {
return string(chars)
}
// lookup and set Go related variables
func initGoPaths() {
// lookup go path
gopath = build.Default.GOPATH
if gopath == "" {
errorf("Abort: GOPATH environment variable is not set. " +
"Please refer to http://golang.org/doc/code.html to configure your Go environment.")
}
// check for go executable
var err error
gocmd, err = exec.LookPath("go")
if err != nil {
errorf("Go executable not found in PATH.")
}
// revel/revel#1004 choose go path relative to current working directory
workingDir, _ := os.Getwd()
goPathList := filepath.SplitList(gopath)
for _, path := range goPathList {
if strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) {
srcRoot = path
break
}
path, _ = filepath.EvalSymlinks(path)
if len(path) > 0 && strings.HasPrefix(strings.ToLower(workingDir), strings.ToLower(path)) {
srcRoot = path
break
}
}
if len(srcRoot) == 0 {
revel.ERROR.Fatalln("Abort: could not create a Revel application outside of GOPATH.")
}
// set go src path
srcRoot = filepath.Join(srcRoot, "src")
}
func setApplicationPath(args []string) {
var err error
importPath = args[0]
// Sets the applicaiton path
func setApplicationPath(c *model.CommandConfig) (err error) {
// revel/revel#1014 validate relative path, we cannot use built-in functions
// since Go import path is valid relative path too.
// so check basic part of the path, which is "."
if filepath.IsAbs(importPath) || strings.HasPrefix(importPath, ".") {
errorf("Abort: '%s' looks like a directory. Please provide a Go import path instead.",
importPath)
}
_, err = build.Import(importPath, "", build.FindOnly)
if err == nil {
errorf("Abort: Import path %s already exists.\n", importPath)
}
revelPkg, err = build.Import(revel.RevelImportPath, "", build.FindOnly)
if err != nil {
errorf("Abort: Could not find Revel source code: %s\n", err)
}
appPath = filepath.Join(srcRoot, filepath.FromSlash(importPath))
appName = filepath.Base(appPath)
basePath = filepath.ToSlash(filepath.Dir(importPath))
if basePath == "." {
// we need to remove the a single '.' when
// the app is in the $GOROOT/src directory
basePath = ""
} else {
// we need to append a '/' when the app is
// is a subdirectory such as $GOROOT/src/path/to/revelapp
basePath += "/"
}
}
func setSkeletonPath(args []string) {
var err error
if len(args) == 2 { // user specified
skeletonName := args[1]
_, err = build.Import(skeletonName, "", build.FindOnly)
// If we are running a vendored version of Revel we do not need to check for it.
if !c.Vendored {
if filepath.IsAbs(c.ImportPath) || strings.HasPrefix(c.ImportPath, ".") {
utils.Logger.Fatalf("Abort: '%s' looks like a directory. Please provide a Go import path instead.",
c.ImportPath)
}
_, err = build.Import(model.RevelImportPath, "", build.FindOnly)
if err != nil {
// Execute "go get <pkg>"
getCmd := exec.Command(gocmd, "get", "-d", skeletonName)
fmt.Println("Exec:", getCmd.Args)
getOutput, err := getCmd.CombinedOutput()
// check getOutput for no buildible string
bpos := bytes.Index(getOutput, []byte("no buildable Go source files in"))
if err != nil && bpos == -1 {
errorf("Abort: Could not find or 'go get' Skeleton source code: %s\n%s\n", getOutput, skeletonName)
//// Go get the revel project
err = c.PackageResolver(model.RevelImportPath)
if err != nil {
return utils.NewBuildIfError(err, "Failed to fetch revel " + model.RevelImportPath)
}
}
// use the
skeletonPath = filepath.Join(srcRoot, skeletonName)
} else {
// use the revel default
revelCmdPkg, err = build.Import(RevelCmdImportPath, "", build.FindOnly)
if err != nil {
errorf("Abort: Could not find Revel Cmd source code: %s\n", err)
}
skeletonPath = filepath.Join(revelCmdPkg.Dir, "revel", "skeleton")
}
c.AppName = filepath.Base(c.AppPath)
return nil
}
func copyNewAppFiles() {
var err error
err = os.MkdirAll(appPath, 0777)
panicOnError(err, "Failed to create directory "+appPath)
// Set the skeleton path
func setSkeletonPath(c *model.CommandConfig) (err error) {
if len(c.New.SkeletonPath) == 0 {
c.New.SkeletonPath = "https://" + RevelSkeletonsImportPath + ":basic/bootstrap4"
}
_ = mustCopyDir(appPath, skeletonPath, map[string]interface{}{
// First check to see the protocol of the string
sp, err := url.Parse(c.New.SkeletonPath)
if err == nil {
utils.Logger.Info("Detected skeleton path", "path", sp)
switch strings.ToLower(sp.Scheme) {
// TODO Add support for ftp, sftp, scp ??
case "" :
sp.Scheme = "file"
fallthrough
case "file" :
fullpath := sp.String()[7:]
if !filepath.IsAbs(fullpath) {
fullpath, err = filepath.Abs(fullpath)
if err != nil {
return
}
}
c.New.SkeletonPath = fullpath
utils.Logger.Info("Set skeleton path to ", fullpath)
if !utils.DirExists(fullpath) {
return fmt.Errorf("Failed to find skeleton in filepath %s %s", fullpath, sp.String())
}
case "git":
fallthrough
case "http":
fallthrough
case "https":
if err := newLoadFromGit(c, sp); err != nil {
return err
}
default:
utils.Logger.Fatal("Unsupported skeleton schema ", "path", c.New.SkeletonPath)
}
// TODO check to see if the path needs to be extracted
} else {
utils.Logger.Fatal("Invalid skeleton path format", "path", c.New.SkeletonPath)
}
return
}
// Load skeleton from git
func newLoadFromGit(c *model.CommandConfig, sp *url.URL) (err error) {
// This method indicates we need to fetch from a repository using git
// Execute "git clone get <pkg>"
targetPath := filepath.Join(os.TempDir(), "revel", "skeleton")
os.RemoveAll(targetPath)
pathpart := strings.Split(sp.Path, ":")
getCmd := exec.Command("git", "clone", sp.Scheme + "://" + sp.Host + pathpart[0], targetPath)
utils.Logger.Info("Exec:", "args", getCmd.Args)
getOutput, err := getCmd.CombinedOutput()
if err != nil {
utils.Logger.Fatal("Abort: could not clone the Skeleton source code: ", "output", string(getOutput), "path", c.New.SkeletonPath)
}
outputPath := targetPath
if len(pathpart) > 1 {
outputPath = filepath.Join(targetPath, filepath.Join(strings.Split(pathpart[1], string('/'))...))
}
outputPath, _ = filepath.Abs(outputPath)
if !strings.HasPrefix(outputPath, targetPath) {
utils.Logger.Fatal("Unusual target path outside root path", "target", outputPath, "root", targetPath)
}
c.New.SkeletonPath = outputPath
return
}
func copyNewAppFiles(c *model.CommandConfig) (err error) {
err = os.MkdirAll(c.AppPath, 0777)
if err != nil {
return utils.NewBuildIfError(err, "MKDIR failed")
}
err = utils.CopyDir(c.AppPath, c.New.SkeletonPath, map[string]interface{}{
// app.conf
"AppName": appName,
"BasePath": basePath,
"AppName": c.AppName,
"BasePath": c.AppPath,
"Secret": generateSecret(),
})
if err != nil {
fmt.Printf("err %v", err)
return utils.NewBuildIfError(err, "Copy Dir failed")
}
// Dotfiles are skipped by mustCopyDir, so we have to explicitly copy the .gitignore.
gitignore := ".gitignore"
mustCopyFile(filepath.Join(appPath, gitignore), filepath.Join(skeletonPath, gitignore))
return utils.CopyFile(filepath.Join(c.AppPath, gitignore), filepath.Join(c.New.SkeletonPath, gitignore))
}
const (
VENDOR_GOPKG = `#
# Revel Gopkg.toml
#
# If you want to use a specific version of Revel change the branches below
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
required = ["github.com/revel/revel", "github.com/revel/modules"]
# Note to use a specific version changes this to
#
# [[override]]
# version = "0.20.1"
# name = "github.com/revel/modules"
[[override]]
branch = "master"
name = "github.com/revel/modules"
# Note to use a specific version changes this to
#
# [[override]]
# version = "0.20.0"
# name = "github.com/revel/revel"
[[override]]
branch = "master"
name = "github.com/revel/revel"
[[override]]
branch = "master"
name = "github.com/revel/log15"
[[override]]
branch = "master"
name = "github.com/revel/cron"
[[override]]
branch = "master"
name = "github.com/xeonx/timeago"
`
NEW_MAIN_FILE = `package main
`
)

55
revel/new_test.go Normal file
View File

@@ -0,0 +1,55 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// test the commands
func TestNew(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-new", a)
t.Run("New", func(t *testing.T) {
a := assert.New(t)
c := newApp("new-test", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "New failed")
})
t.Run("New-NotVendoredmode", func(t *testing.T) {
a := assert.New(t)
c := newApp("new-notvendored", model.NEW, nil, a)
c.New.NotVendored = true
a.Nil(main.Commands[model.NEW].RunWith(c), "New failed")
})
t.Run("Path", func(t *testing.T) {
a := assert.New(t)
c := newApp("new/test/a", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "New path failed")
})
t.Run("Path-Duplicate", func(t *testing.T) {
a := assert.New(t)
c := newApp("new/test/b", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "New path failed")
c = newApp("new/test/b", model.NEW, nil, a)
a.NotNil(main.Commands[model.NEW].RunWith(c), "Duplicate path Did Not failed")
})
t.Run("Skeleton-Git", func(t *testing.T) {
a := assert.New(t)
c := newApp("new/test/c/1", model.NEW, nil, a)
c.New.SkeletonPath = "git://github.com/revel/skeletons:basicnsadnsak"
a.NotNil(main.Commands[model.NEW].RunWith(c), "Expected Failed to run with new")
// We need to pick a different path
c = newApp("new/test/c/2", model.NEW, nil, a)
c.New.SkeletonPath = "git://github.com/revel/skeletons:basic/bootstrap4"
a.Nil(main.Commands[model.NEW].RunWith(c), "Failed to run with new skeleton git")
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

View File

@@ -10,11 +10,12 @@ import (
"os"
"path/filepath"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
)
var cmdPackage = &Command{
UsageLine: "package [import path] [run mode]",
UsageLine: "package [-r [run mode]] [application] ",
Short: "package a Revel application (e.g. for deployment)",
Long: `
Package the Revel web application named by the given import path.
@@ -32,38 +33,73 @@ For example:
}
func init() {
cmdPackage.Run = packageApp
cmdPackage.RunWith = packageApp
cmdPackage.UpdateConfig = updatePackageConfig
}
func packageApp(args []string) {
if len(args) == 0 {
fmt.Fprint(os.Stderr, cmdPackage.Long)
return
// Called when unable to parse the command line automatically and assumes an old launch
func updatePackageConfig(c *model.CommandConfig, args []string) bool {
c.Index = model.PACKAGE
if len(args)==0 && c.Package.ImportPath!="" {
return true
}
c.Package.ImportPath = args[0]
if len(args) > 1 {
c.Package.Mode = args[1]
}
return true
}
// Called to package the app
func packageApp(c *model.CommandConfig) (err error) {
// Determine the run mode.
mode := DefaultRunMode
if len(args) >= 2 {
mode = args[1]
if len(c.Package.Mode) >= 0 {
mode = c.Package.Mode
}
appImportPath := args[0]
revel.Init(mode, appImportPath, "")
appImportPath := c.ImportPath
revel_paths, err := model.NewRevelPaths(mode, appImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
if err != nil {
return
}
// Remove the archive if it already exists.
destFile := filepath.Base(revel.BasePath) + ".tar.gz"
destFile := filepath.Join(c.AppPath, filepath.Base(revel_paths.BasePath)+".tar.gz")
if c.Package.TargetPath != "" {
if filepath.IsAbs(c.Package.TargetPath) {
destFile = c.Package.TargetPath
} else {
destFile = filepath.Join(c.AppPath, c.Package.TargetPath)
}
}
if err := os.Remove(destFile); err != nil && !os.IsNotExist(err) {
revel.ERROR.Fatal(err)
return utils.NewBuildError("Unable to remove target file", "error", err, "file", destFile)
}
// Collect stuff in a temp directory.
tmpDir, err := ioutil.TempDir("", filepath.Base(revel.BasePath))
panicOnError(err, "Failed to get temp dir")
tmpDir, err := ioutil.TempDir("", filepath.Base(revel_paths.BasePath))
utils.PanicOnError(err, "Failed to get temp dir")
buildApp([]string{args[0], tmpDir, mode})
// Build expects the command the build to contain the proper data
if len(c.Package.Mode) >= 0 {
c.Build.Mode = c.Package.Mode
}
c.Build.TargetPath = tmpDir
c.Build.CopySource = c.Package.CopySource
if err = buildApp(c); err != nil {
return
}
// Create the zip file.
archiveName := mustTarGzDir(destFile, tmpDir)
archiveName, err := utils.TarGzDir(destFile, tmpDir)
if err != nil {
return
}
fmt.Println("Your archive is ready:", archiveName)
return
}

View File

@@ -1,2 +0,0 @@
@echo off
{{.BinName}} -importPath {{.ImportPath}} -srcPath %CD%\src -runMode {{.Mode}}

View File

@@ -1,3 +0,0 @@
#!/bin/sh
SCRIPTPATH=$(cd "$(dirname "$0")"; pwd)
"$SCRIPTPATH/{{.BinName}}" -importPath {{.ImportPath}} -srcPath "$SCRIPTPATH/src" -runMode {{.Mode}}

30
revel/package_test.go Normal file
View File

@@ -0,0 +1,30 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// test the commands
func TestPackage(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-package", a)
t.Run("Package", func(t *testing.T) {
a := assert.New(t)
c := newApp("package-test", model.NEW, nil, a)
main.Commands[model.NEW].RunWith(c)
c.Index = model.PACKAGE
c.Package.ImportPath = c.ImportPath
a.Nil(main.Commands[model.PACKAGE].RunWith(c), "Failed to run package-test")
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

View File

@@ -1,144 +0,0 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// The command line tool for running Revel apps.
package main
import (
"flag"
"fmt"
"io"
"math/rand"
"os"
"runtime"
"strings"
"text/template"
"time"
"github.com/agtorre/gocolorize"
)
const (
// RevelCmdImportPath Revel framework cmd tool import path
RevelCmdImportPath = "github.com/revel/cmd"
// DefaultRunMode for revel's application
DefaultRunMode = "dev"
)
// Command structure cribbed from the genius organization of the "go" command.
type Command struct {
Run func(args []string)
UsageLine, Short, Long string
}
// Name returns command name from usage line
func (cmd *Command) Name() string {
name := cmd.UsageLine
i := strings.Index(name, " ")
if i >= 0 {
name = name[:i]
}
return name
}
var commands = []*Command{
cmdNew,
cmdRun,
cmdBuild,
cmdPackage,
cmdClean,
cmdTest,
cmdVersion,
}
func main() {
if runtime.GOOS == "windows" {
gocolorize.SetPlain(true)
}
fmt.Fprintf(os.Stdout, gocolorize.NewColor("blue").Paint(header))
flag.Usage = func() { usage(1) }
flag.Parse()
args := flag.Args()
if len(args) < 1 || args[0] == "help" {
if len(args) == 1 {
usage(0)
}
if len(args) > 1 {
for _, cmd := range commands {
if cmd.Name() == args[1] {
tmpl(os.Stdout, helpTemplate, cmd)
return
}
}
}
usage(2)
}
// Commands use panic to abort execution when something goes wrong.
// Panics are logged at the point of error. Ignore those.
defer func() {
if err := recover(); err != nil {
if _, ok := err.(LoggedError); !ok {
// This panic was not expected / logged.
panic(err)
}
os.Exit(1)
}
}()
for _, cmd := range commands {
if cmd.Name() == args[0] {
cmd.Run(args[1:])
return
}
}
errorf("unknown command %q\nRun 'revel help' for usage.\n", args[0])
}
func errorf(format string, args ...interface{}) {
// Ensure the user's command prompt starts on the next line.
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, format, args...)
panic(LoggedError{}) // Panic instead of os.Exit so that deferred will run.
}
const header = `~
~ revel! http://revel.github.io
~
`
const usageTemplate = `usage: revel command [arguments]
The commands are:
{{range .}}
{{.Name | printf "%-11s"}} {{.Short}}{{end}}
Use "revel help [command]" for more information.
`
var helpTemplate = `usage: revel {{.UsageLine}}
{{.Long}}
`
func usage(exitCode int) {
tmpl(os.Stderr, usageTemplate, commands)
os.Exit(exitCode)
}
func tmpl(w io.Writer, text string, data interface{}) {
t := template.New("top")
template.Must(t.Parse(text))
if err := t.Execute(w, data); err != nil {
panic(err)
}
}
func init() {
rand.Seed(time.Now().UnixNano())
}

153
revel/revel.go Normal file
View File

@@ -0,0 +1,153 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// The command line tool for running Revel apps.
package main
import (
"flag"
"fmt"
"math/rand"
"os"
"runtime"
"strings"
"time"
"github.com/jessevdk/go-flags"
"github.com/agtorre/gocolorize"
"github.com/revel/cmd/logger"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"bytes"
)
const (
// RevelCmdImportPath Revel framework cmd tool import path
RevelCmdImportPath = "github.com/revel/cmd"
// RevelCmdImportPath Revel framework cmd tool import path
RevelSkeletonsImportPath = "github.com/revel/skeletons"
// DefaultRunMode for revel's application
DefaultRunMode = "dev"
)
// Command structure cribbed from the genius organization of the "go" command.
type Command struct {
UpdateConfig func(c *model.CommandConfig, args []string) bool
RunWith func(c *model.CommandConfig) error
UsageLine, Short, Long string
}
// Name returns command name from usage line
func (cmd *Command) Name() string {
name := cmd.UsageLine
i := strings.Index(name, " ")
if i >= 0 {
name = name[:i]
}
return name
}
// The commands
var Commands = []*Command{
nil, // Safety net, prevent missing index from running
cmdNew,
cmdRun,
cmdBuild,
cmdPackage,
cmdClean,
cmdTest,
cmdVersion,
}
func main() {
if runtime.GOOS == "windows" {
gocolorize.SetPlain(true)
}
c := &model.CommandConfig{}
wd, _ := os.Getwd()
utils.InitLogger(wd, logger.LvlError)
parser := flags.NewParser(c, flags.HelpFlag | flags.PassDoubleDash)
if len(os.Args) < 2 {
parser.WriteHelp(os.Stdout)
os.Exit(1)
}
if err := ParseArgs(c, parser, os.Args[1:]); err != nil {
fmt.Fprint(os.Stderr, err.Error() + "\n")
os.Exit(1)
}
// Switch based on the verbose flag
if len(c.Verbose) > 1 {
utils.InitLogger(wd, logger.LvlDebug)
} else if len(c.Verbose) > 0 {
utils.InitLogger(wd, logger.LvlInfo)
} else {
utils.InitLogger(wd, logger.LvlWarn)
}
// Setup package resolver
c.InitPackageResolver()
if err := c.UpdateImportPath(); err != nil {
utils.Logger.Error(err.Error())
parser.WriteHelp(os.Stdout)
os.Exit(1)
}
command := Commands[c.Index]
println("Revel executing:", command.Short)
if err := command.RunWith(c); err != nil {
utils.Logger.Error("Unable to execute", "error", err)
os.Exit(1)
}
}
// Parse the arguments passed into the model.CommandConfig
func ParseArgs(c *model.CommandConfig, parser *flags.Parser, args []string) (err error) {
var extraArgs []string
if ini := flag.String("ini", "none", ""); *ini != "none" {
if err = flags.NewIniParser(parser).ParseFile(*ini); err != nil {
return
}
} else {
if extraArgs, err = parser.ParseArgs(args); err != nil {
return
} else {
switch parser.Active.Name {
case "new":
c.Index = model.NEW
case "run":
c.Index = model.RUN
case "build":
c.Index = model.BUILD
case "package":
c.Index = model.PACKAGE
case "clean":
c.Index = model.CLEAN
case "test":
c.Index = model.TEST
case "version":
c.Index = model.VERSION
}
}
}
if !Commands[c.Index].UpdateConfig(c, extraArgs) {
buffer := &bytes.Buffer{}
parser.WriteHelp(buffer)
err = fmt.Errorf("Invalid command line arguements %v\n%s", extraArgs, buffer.String())
}
return
}
func init() {
rand.Seed(time.Now().UnixNano())
}

View File

@@ -5,31 +5,33 @@
package main
import (
"go/build"
"strconv"
"encoding/json"
"fmt"
"github.com/revel/cmd/harness"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"os"
)
var cmdRun = &Command{
UsageLine: "run [import path] [run mode] [port]",
UsageLine: "run [-m [run mode] -p [port]] [import path] ",
Short: "run a Revel application",
Long: `
Run the Revel web application named by the given import path.
For example, to run the chat room sample application:
revel run github.com/revel/examples/chat dev
revel run -m dev github.com/revel/examples/chat
The run mode is used to select which set of app.conf configuration should
apply and may be used to determine logic in the application itself.
Run mode defaults to "dev".
You can set a port as an optional third parameter. For example:
You can set a port as well. For example:
revel run github.com/revel/examples/chat prod 8080`,
revel run -m prod -p 8080 github.com/revel/examples/chat `,
}
// RunArgs holds revel run parameters
@@ -40,98 +42,126 @@ type RunArgs struct {
}
func init() {
cmdRun.Run = runApp
cmdRun.RunWith = runApp
cmdRun.UpdateConfig = updateRunConfig
}
func parseRunArgs(args []string) *RunArgs {
inputArgs := RunArgs{
ImportPath: importPathFromCurrentDir(),
Mode: DefaultRunMode,
Port: revel.HTTPPort,
func updateRunConfig(c *model.CommandConfig, args []string) bool {
convertPort := func(value string) int {
if value != "" {
port, err := strconv.Atoi(value)
if err != nil {
utils.Logger.Fatalf("Failed to parse port as integer: %s", c.Run.Port)
}
return port
}
return 0
}
switch len(args) {
case 3:
// Possibile combinations
// Possible combinations
// revel run [import-path] [run-mode] [port]
port, err := strconv.Atoi(args[2])
if err != nil {
errorf("Failed to parse port as integer: %s", args[2])
}
inputArgs.ImportPath = args[0]
inputArgs.Mode = args[1]
inputArgs.Port = port
c.Run.ImportPath = args[0]
c.Run.Mode = args[1]
c.Run.Port = convertPort(args[2])
case 2:
// Possibile combinations
// Possible combinations
// 1. revel run [import-path] [run-mode]
// 2. revel run [import-path] [port]
// 3. revel run [run-mode] [port]
if _, err := build.Import(args[0], "", build.FindOnly); err == nil {
// Check to see if the import path evaluates out to something that may be on a gopath
if runIsImportPath(args[0]) {
// 1st arg is the import path
inputArgs.ImportPath = args[0]
if port, err := strconv.Atoi(args[1]); err == nil {
c.Run.ImportPath = args[0]
if _, err := strconv.Atoi(args[1]); err == nil {
// 2nd arg is the port number
inputArgs.Port = port
c.Run.Port = convertPort(args[1])
} else {
// 2nd arg is the run mode
inputArgs.Mode = args[1]
c.Run.Mode = args[1]
}
} else {
// 1st arg is the run mode
port, err := strconv.Atoi(args[1])
if err != nil {
errorf("Failed to parse port as integer: %s", args[1])
}
inputArgs.Mode = args[0]
inputArgs.Port = port
c.Run.Mode = args[0]
c.Run.Port = convertPort(args[1])
}
case 1:
// Possibile combinations
// Possible combinations
// 1. revel run [import-path]
// 2. revel run [port]
// 3. revel run [run-mode]
if _, err := build.Import(args[0], "", build.FindOnly); err == nil {
if runIsImportPath(args[0]) {
// 1st arg is the import path
inputArgs.ImportPath = args[0]
} else if port, err := strconv.Atoi(args[0]); err == nil {
c.Run.ImportPath = args[0]
} else if _, err := strconv.Atoi(args[0]); err == nil {
// 1st arg is the port number
inputArgs.Port = port
c.Run.Port = convertPort(args[0])
} else {
// 1st arg is the run mode
inputArgs.Mode = args[0]
c.Run.Mode = args[0]
}
case 0:
// Attempt to set the import path to the current working director.
if c.Run.ImportPath == "" {
c.Run.ImportPath, _ = os.Getwd()
}
}
return &inputArgs
c.Index = model.RUN
return true
}
func runApp(args []string) {
runArgs := parseRunArgs(args)
// Returns true if this is an absolute path or a relative gopath
func runIsImportPath(pathToCheck string) bool {
return utils.DirExists(pathToCheck)
}
// Find and parse app.conf
revel.Init(runArgs.Mode, runArgs.ImportPath, "")
revel.LoadMimeConfig()
// fallback to default port
if runArgs.Port == 0 {
runArgs.Port = revel.HTTPPort
// Called to run the app
func runApp(c *model.CommandConfig) (err error) {
if c.Run.Mode == "" {
c.Run.Mode = "dev"
}
revel.INFO.Printf("Running %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, runArgs.Mode)
revel.TRACE.Println("Base path:", revel.BasePath)
revel_path, err := model.NewRevelPaths(c.Run.Mode, c.ImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
if err != nil {
return utils.NewBuildIfError(err, "Revel paths")
}
if c.Run.Port > -1 {
revel_path.HTTPPort = c.Run.Port
} else {
c.Run.Port = revel_path.HTTPPort
}
utils.Logger.Infof("Running %s (%s) in %s mode\n", revel_path.AppName, revel_path.ImportPath, revel_path.RunMode)
utils.Logger.Debug("Base path:", "path", revel_path.BasePath)
// If the app is run in "watched" mode, use the harness to run it.
if revel.Config.BoolDefault("watch", true) && revel.Config.BoolDefault("watch.code", true) {
revel.TRACE.Println("Running in watched mode.")
revel.HTTPPort = runArgs.Port
harness.NewHarness().Run() // Never returns.
if revel_path.Config.BoolDefault("watch", true) && revel_path.Config.BoolDefault("watch.code", true) {
utils.Logger.Info("Running in watched mode.")
runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v}`, revel_path.RunMode, c.Verbose)
if c.HistoricMode {
runMode = revel_path.RunMode
}
// **** Never returns.
harness.NewHarness(c, revel_path, runMode, c.Run.NoProxy).Run()
}
// Else, just build and run the app.
revel.TRACE.Println("Running in live build mode.")
app, err := harness.Build()
utils.Logger.Debug("Running in live build mode.")
app, err := harness.Build(c, revel_path)
if err != nil {
errorf("Failed to build app: %s", err)
utils.Logger.Errorf("Failed to build app: %s", err)
}
app.Port = runArgs.Port
app.Cmd().Run()
app.Port = revel_path.HTTPPort
var paths []byte
if len(app.PackagePathMap) > 0 {
paths, _ = json.Marshal(app.PackagePathMap)
}
runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, app.Paths.RunMode, c.Verbose, string(paths))
if c.HistoricMode {
runMode = revel_path.RunMode
}
app.Cmd(runMode).Run()
return
}

21
revel/run_test.go Normal file
View File

@@ -0,0 +1,21 @@
package main_test
import (
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// test the commands
func TestRun(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-run", a)
// TODO Testing run
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

View File

@@ -1,3 +0,0 @@
test-results/
tmp/
routes/

View File

@@ -1,43 +0,0 @@
# Welcome to Revel
A high-productivity web framework for the [Go language](http://www.golang.org/).
### Start the web server:
revel run myapp
### Go to http://localhost:9000/ and you'll see:
"It works"
## Code Layout
The directory structure of a generated Revel application:
conf/ Configuration directory
app.conf Main app configuration file
routes Routes definition file
app/ App sources
init.go Interceptor registration
controllers/ App controllers go here
views/ Templates directory
messages/ Message files
public/ Public static assets
css/ CSS files
js/ Javascript files
images/ Image files
tests/ Test suites
## Help
* The [Getting Started with Revel](http://revel.github.io/tutorial/gettingstarted.html).
* The [Revel guides](http://revel.github.io/manual/index.html).
* The [Revel sample apps](http://revel.github.io/examples/index.html).
* The [API documentation](https://godoc.org/github.com/revel/revel).

View File

@@ -1,13 +0,0 @@
package controllers
import (
"github.com/revel/revel"
)
type App struct {
*revel.Controller
}
func (c App) Index() revel.Result {
return c.Render()
}

View File

@@ -1,59 +0,0 @@
package app
import (
"github.com/revel/revel"
)
var (
// AppVersion revel app version (ldflags)
AppVersion string
// BuildTime revel app build-time (ldflags)
BuildTime string
)
func init() {
// Filters is the default set of global filters.
revel.Filters = []revel.Filter{
revel.PanicFilter, // Recover from panics and display an error page instead.
revel.RouterFilter, // Use the routing table to select the right Action
revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
revel.ParamsFilter, // Parse parameters into Controller.Params.
revel.SessionFilter, // Restore and write the session cookie.
revel.FlashFilter, // Restore and write the flash cookie.
revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie.
revel.I18nFilter, // Resolve the requested language
HeaderFilter, // Add some security based headers
revel.InterceptorFilter, // Run interceptors around the action.
revel.CompressFilter, // Compress the result.
revel.ActionInvoker, // Invoke the action.
}
// register startup functions with OnAppStart
// revel.DevMode and revel.RunMode only work inside of OnAppStart. See Example Startup Script
// ( order dependent )
// revel.OnAppStart(ExampleStartupScript)
// revel.OnAppStart(InitDB)
// revel.OnAppStart(FillCache)
}
// HeaderFilter adds common security headers
// TODO turn this into revel.HeaderFilter
// should probably also have a filter for CSRF
// not sure if it can go in the same filter or not
var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) {
c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN")
c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block")
c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff")
fc[0](c, fc[1:]) // Execute the next filter stage.
}
//func ExampleStartupScript() {
// // revel.DevMod and revel.RunMode work here
// // Use this script to check for dev mode and set dev/prod startup scripts here!
// if revel.DevMode == true {
// // Dev mode
// }
//}

View File

@@ -1,21 +0,0 @@
{{set . "title" "Home"}}
{{template "header.html" .}}
<header class="jumbotron" style="background-color:#A9F16C">
<div class="container">
<div class="row">
<h1>It works!</h1>
<p></p>
</div>
</div>
</header>
<div class="container">
<div class="row">
<div class="span6">
{{template "flash.html" .}}
</div>
</div>
</div>
{{template "footer.html" .}}

View File

@@ -1,64 +0,0 @@
<style type="text/css">
#sidebar {
position: absolute;
right: 0px;
top:69px;
max-width: 75%;
z-index: 1000;
background-color: #fee;
border: thin solid grey;
padding: 10px;
}
#toggleSidebar {
position: absolute;
right: 0px;
top: 50px;
background-color: #fee;
}
</style>
<div id="sidebar" style="display:none;">
<h4>Available pipelines</h4>
<dl>
{{ range $index, $value := .}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>
<h4>Flash</h4>
<dl>
{{ range $index, $value := .flash}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>
<h4>Errors</h4>
<dl>
{{ range $index, $value := .errors}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>
</div>
<a id="toggleSidebar" href="#" class="toggles"><i class="glyphicon glyphicon-chevron-left"></i></a>
<script>
$sidebar = 0;
$('#toggleSidebar').click(function() {
if ($sidebar === 1) {
$('#sidebar').hide();
$('#toggleSidebar i').addClass('glyphicon-chevron-left');
$('#toggleSidebar i').removeClass('glyphicon-chevron-right');
$sidebar = 0;
}
else {
$('#sidebar').show();
$('#toggleSidebar i').addClass('glyphicon-chevron-right');
$('#toggleSidebar i').removeClass('glyphicon-chevron-left');
$sidebar = 1;
}
return false;
});
</script>

View File

@@ -1,20 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Not found</title>
</head>
<body>
{{if eq .RunMode "dev"}}
{{template "errors/404-dev.html" .}}
{{else}}
{{with .Error}}
<h1>
{{.Title}}
</h1>
<p>
{{.Description}}
</p>
{{end}}
{{end}}
</body>
</html>

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Application error</title>
</head>
<body>
{{if eq .RunMode "dev"}}
{{template "errors/500-dev.html" .}}
{{else}}
<h1>Oops, an error occured</h1>
<p>
This exception has been logged.
</p>
{{end}}
</body>
</html>

View File

@@ -1,18 +0,0 @@
{{if .flash.success}}
<div class="alert alert-success">
{{.flash.success}}
</div>
{{end}}
{{if or .errors .flash.error}}
<div class="alert alert-danger">
{{if .flash.error}}
{{.flash.error}}
{{end}}
<ul style="margin-top:10px;">
{{range .errors}}
<li>{{.}}</li>
{{end}}
</ul>
</div>
{{end}}

View File

@@ -1,5 +0,0 @@
{{if eq .RunMode "dev"}}
{{template "debug.html" .}}
{{end}}
</body>
</html>

View File

@@ -1,19 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/public/css/bootstrap-3.3.6.min.css">
<link rel="shortcut icon" type="image/png" href="/public/img/favicon.png">
<script src="/public/js/jquery-2.2.4.min.js"></script>
<script src="/public/js/bootstrap-3.3.6.min.js"></script>
{{range .moreStyles}}
<link rel="stylesheet" type="text/css" href="/public/{{.}}">
{{end}}
{{range .moreScripts}}
<script src="/public/{{.}}" type="text/javascript" charset="utf-8"></script>
{{end}}
</head>
<body>

View File

@@ -1,217 +0,0 @@
################################################################################
# Revel configuration file
# More info at http://revel.github.io/manual/appconf.html
################################################################################
# Sets the `AppName` variable which can be used in your code as
# `if revel.AppName {...}`
app.name = {{ .AppName }}
# A secret string which is passed to cryptographically sign the cookie to prevent
# (and detect) user modification.
# Keep this string secret or users will be able to inject arbitrary cookie values
# into your application
app.secret = {{ .Secret }}
# Revel running behind proxy like nginx, haproxy, etc
app.behind.proxy = false
# The IP address on which to listen.
http.addr =
# The port on which to listen.
http.port = 9000
# Whether to use SSL or not.
http.ssl = false
# Path to an X509 certificate file, if using SSL.
#http.sslcert =
# Path to an X509 certificate key, if using SSL.
#http.sslkey =
# Timeout specifies a time limit for request (in seconds) made by a single client.
# A Timeout of zero means no timeout.
http.timeout.read = 90
http.timeout.write = 60
# For any cookies set by Revel (Session,Flash,Error) these properties will set
# the fields of:
# http://golang.org/pkg/net/http/#Cookie
#
# Each cookie set by Revel is prefixed with this string.
cookie.prefix = REVEL
# A secure cookie has the secure attribute enabled and is only used via HTTPS,
# ensuring that the cookie is always encrypted when transmitting from client to
# server. This makes the cookie less likely to be exposed to cookie theft via
# eavesdropping.
#
# Defaults to false. If 'http.ssl' is enabled, this will be defaulted to true.
# This should only be true when Revel is handling SSL connections. If you are
# using a proxy in front of revel (Nginx, Apache, etc), then this should be left
# as false.
# cookie.secure = false
# Limit cookie access to a given domain
#cookie.domain =
# Define when your session cookie expires. Possible values:
# "720h"
# A time duration (http://golang.org/pkg/time/#ParseDuration) after which
# the cookie expires and the session is invalid.
# "session"
# Sets a session cookie which invalidates the session when the user close
# the browser.
session.expires = 720h
# The date format used by Revel. Possible formats defined by the Go `time`
# package (http://golang.org/pkg/time/#Parse)
format.date = 2006-01-02
format.datetime = 2006-01-02 15:04
# Determines whether the template rendering should use chunked encoding.
# Chunked encoding can decrease the time to first byte on the client side by
# sending data before the entire template has been fully rendered.
results.chunked = false
# Prefixes for each log message line
# User can override these prefix values within any section
# For e.g: [dev], [prod], etc
log.trace.prefix = "TRACE "
log.info.prefix = "INFO "
log.warn.prefix = "WARN "
log.error.prefix = "ERROR "
# The default language of this application.
i18n.default_language = en
# The default format when message is missing.
# The original message shows in %s
#i18n.unknown_format = "??? %s ???"
# Module to serve static content such as CSS, JavaScript and Media files
# Allows Routes like this:
# `Static.ServeModule("modulename","public")`
module.static=github.com/revel/modules/static
################################################################################
# Section: dev
# This section is evaluated when running Revel in dev mode. Like so:
# `revel run path/to/myapp`
[dev]
# This sets `DevMode` variable to `true` which can be used in your code as
# `if revel.DevMode {...}`
# or in your templates with
# `{{.DevMode}}`
mode.dev = true
# Pretty print JSON/XML when calling RenderJSON/RenderXML
results.pretty = true
# Automatically watches your applicaton files and recompiles on-demand
watch = true
# If you set watch.mode = "eager", the server starts to recompile
# your application every time your application's files change.
watch.mode = "normal"
# Watch the entire $GOPATH for code changes. Default is false.
#watch.gopath = true
# Module to run code tests in the browser
# See:
# http://revel.github.io/manual/testing.html
module.testrunner = github.com/revel/modules/testrunner
# Where to log the various Revel logs
log.trace.output = off
log.info.output = stderr
log.warn.output = stderr
log.error.output = stderr
# Revel log flags. Possible flags defined by the Go `log` package,
# please refer https://golang.org/pkg/log/#pkg-constants
# Go log is "Bits or'ed together to control what's printed"
# Examples:
# 0 => just log the message, turn off the flags
# 3 => log.LstdFlags (log.Ldate|log.Ltime)
# 19 => log.Ldate|log.Ltime|log.Lshortfile
# 23 => log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile
log.trace.flags = 19
log.info.flags = 19
log.warn.flags = 19
log.error.flags = 19
# Revel request access log
# Access log line format:
# RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath
# Sample format:
# 2016/05/25 17:46:37.112 127.0.0.1 200 270.157µs GET /
log.request.output = stderr
################################################################################
# Section: prod
# This section is evaluated when running Revel in production mode. Like so:
# `revel run path/to/myapp prod`
# See:
# [dev] section for documentation of the various settings
[prod]
mode.dev = false
results.pretty = false
watch = false
module.testrunner =
log.trace.output = off
log.info.output = off
log.warn.output = log/%(app.name)s.log
log.error.output = log/%(app.name)s.log
# Revel log flags. Possible flags defined by the Go `log` package,
# please refer https://golang.org/pkg/log/#pkg-constants
# Go log is "Bits or'ed together to control what's printed"
# Examples:
# 0 => just log the message, turn off the flags
# 3 => log.LstdFlags (log.Ldate|log.Ltime)
# 19 => log.Ldate|log.Ltime|log.Lshortfile
# 23 => log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile
log.trace.flags = 3
log.info.flags = 3
log.warn.flags = 3
log.error.flags = 3
# Revel request access log
# Access log line format:
# RequestStartTime ClientIP ResponseStatus RequestLatency HTTPMethod URLPath
# Sample format:
# 2016/05/25 17:46:37.112 127.0.0.1 200 270.157µs GET /
# Example:
# log.request.output = %(app.name)s-request.log
log.request.output = off

View File

@@ -1,19 +0,0 @@
# Routes Config
#
# This file defines all application routes (Higher priority routes first)
#
module:testrunner
# module:jobs
GET / App.Index
# Ignore favicon requests
GET /favicon.ico 404
# Map static resources from the /app/public folder to the /public path
GET /public/*filepath Static.Serve("public")
# Catch all
* /:controller/:action :controller.:action

View File

@@ -1,7 +0,0 @@
# Sample messages file for the English language (en)
# Message file extensions should be ISO 639-1 codes (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
# Sections within each message file can optionally override the defaults using ISO 3166-1 alpha-2 codes (http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)
# See also:
# - http://www.rfc-editor.org/rfc/bcp/bcp47.txt
# - http://www.w3.org/International/questions/qa-accept-lang-locales

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +0,0 @@
package tests
import (
"github.com/revel/revel/testing"
)
type AppTest struct {
testing.TestSuite
}
func (t *AppTest) Before() {
println("Set up")
}
func (t *AppTest) TestThatIndexPageWorks() {
t.Get("/")
t.AssertOk()
t.AssertContentType("text/html; charset=utf-8")
}
func (t *AppTest) After() {
println("Tear down")
}

View File

@@ -16,12 +16,13 @@ import (
"time"
"github.com/revel/cmd/harness"
"github.com/revel/modules/testrunner/app/controllers"
"github.com/revel/revel"
"github.com/revel/cmd/model"
"github.com/revel/cmd/tests"
"github.com/revel/cmd/utils"
)
var cmdTest = &Command{
UsageLine: "test [import path] [run mode] [suite.method]",
UsageLine: "test <import path> [<run mode> <suite.method>]",
Short: "run all tests from the command-line",
Long: `
Run all tests for the Revel app named by the given import path.
@@ -47,70 +48,113 @@ or one of UserTest's methods:
}
func init() {
cmdTest.Run = testApp
cmdTest.RunWith = testApp
cmdTest.UpdateConfig = updateTestConfig
}
func testApp(args []string) {
var err error
if len(args) == 0 {
errorf("No import path given.\nRun 'revel help test' for usage.\n")
// Called to update the config command with from the older stype
func updateTestConfig(c *model.CommandConfig, args []string) bool {
c.Index = model.TEST
if len(args) == 0 && c.Test.ImportPath != "" {
return true
}
// The full test runs
// revel test <import path> (run mode) (suite(.function))
if len(args) < 1 {
return false
}
c.Test.ImportPath = args[0]
if len(args) > 1 {
c.Test.Mode = args[1]
}
if len(args) > 2 {
c.Test.Function = args[2]
}
return true
}
// Called to test the application
func testApp(c *model.CommandConfig) (err error) {
mode := DefaultRunMode
if len(args) >= 2 {
mode = args[1]
if c.Test.Mode != "" {
mode = c.Test.Mode
}
// Find and parse app.conf
revel.Init(mode, args[0], "")
revel_path, err := model.NewRevelPaths(mode, c.ImportPath, c.AppPath, model.NewWrappedRevelCallback(nil, c.PackageResolver))
if err != nil {
return
}
// Ensure that the testrunner is loaded in this mode.
checkTestRunner()
// todo Ensure that the testrunner is loaded in this mode.
// Create a directory to hold the test result files.
resultPath := filepath.Join(revel.BasePath, "test-results")
resultPath := filepath.Join(revel_path.BasePath, "test-results")
if err = os.RemoveAll(resultPath); err != nil {
errorf("Failed to remove test result directory %s: %s", resultPath, err)
return utils.NewBuildError("Failed to remove test result directory ", "path", resultPath, "error", err)
}
if err = os.Mkdir(resultPath, 0777); err != nil {
errorf("Failed to create test result directory %s: %s", resultPath, err)
return utils.NewBuildError("Failed to create test result directory ", "path", resultPath, "error", err)
}
// Direct all the output into a file in the test-results directory.
file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
file, err := os.OpenFile(filepath.Join(resultPath, "app.log"), os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0666)
if err != nil {
errorf("Failed to create test result log file: %s", err)
return utils.NewBuildError("Failed to create test result log file: ", "error", err)
}
app, reverr := harness.Build()
app, reverr := harness.Build(c, revel_path)
if reverr != nil {
errorf("Error building: %s", reverr)
return utils.NewBuildIfError(reverr, "Error building: ")
}
cmd := app.Cmd()
var paths []byte
if len(app.PackagePathMap) > 0 {
paths, _ = json.Marshal(app.PackagePathMap)
}
runMode := fmt.Sprintf(`{"mode":"%s", "specialUseFlag":%v,"packagePathMap":%s}`, app.Paths.RunMode, c.Verbose, string(paths))
if c.HistoricMode {
runMode = app.Paths.RunMode
}
cmd := app.Cmd(runMode)
cmd.Dir = c.AppPath
cmd.Stderr = io.MultiWriter(cmd.Stderr, file)
cmd.Stdout = io.MultiWriter(cmd.Stderr, file)
// Start the app...
if err := cmd.Start(); err != nil {
errorf("%s", err)
if err := cmd.Start(c); err != nil {
return utils.NewBuildError("Unable to start server", "error", err)
}
defer cmd.Kill()
revel.INFO.Printf("Testing %s (%s) in %s mode\n", revel.AppName, revel.ImportPath, mode)
var httpAddr = revel_path.HTTPAddr
if httpAddr == "" {
httpAddr = "localhost"
}
var httpProto = "http"
if revel_path.HTTPSsl {
httpProto = "https"
}
// Get a list of tests
var baseURL = fmt.Sprintf("http://127.0.0.1:%d", revel.HTTPPort)
var baseURL = fmt.Sprintf("%s://%s:%d", httpProto, httpAddr, revel_path.HTTPPort)
utils.Logger.Infof("Testing %s (%s) in %s mode URL %s \n", revel_path.AppName, revel_path.ImportPath, mode, baseURL)
testSuites, _ := getTestsList(baseURL)
// If a specific TestSuite[.Method] is specified, only run that suite/test
if len(args) == 3 {
testSuites = filterTestSuites(testSuites, args[2])
if c.Test.Function != "" {
testSuites = filterTestSuites(testSuites, c.Test.Function)
}
testSuiteCount := len(*testSuites)
fmt.Printf("\n%d test suite%s to run.\n", testSuiteCount, pluralize(testSuiteCount, "", "s"))
fmt.Println()
// Run each suite.
failedResults, overallSuccess := runTestSuites(baseURL, resultPath, testSuites)
failedResults, overallSuccess := runTestSuites(revel_path, baseURL, resultPath, testSuites)
fmt.Println()
if overallSuccess {
@@ -127,16 +171,20 @@ func testApp(args []string) {
}
}
writeResultFile(resultPath, "result.failed", "failed")
errorf("Some tests failed. See file://%s for results.", resultPath)
utils.Logger.Errorf("Some tests failed. See file://%s for results.", resultPath)
}
return
}
// Outputs the results to a file
func writeResultFile(resultPath, name, content string) {
if err := ioutil.WriteFile(filepath.Join(resultPath, name), []byte(content), 0666); err != nil {
errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err)
utils.Logger.Errorf("Failed to write result file %s: %s", filepath.Join(resultPath, name), err)
}
}
// Determines if response should be plural
func pluralize(num int, singular, plural string) string {
if num == 1 {
return singular
@@ -146,7 +194,7 @@ func pluralize(num int, singular, plural string) string {
// Filters test suites and individual tests to match
// the parsed command line parameter
func filterTestSuites(suites *[]controllers.TestSuiteDesc, suiteArgument string) *[]controllers.TestSuiteDesc {
func filterTestSuites(suites *[]tests.TestSuiteDesc, suiteArgument string) *[]tests.TestSuiteDesc {
var suiteName, testName string
argArray := strings.Split(suiteArgument, ".")
suiteName = argArray[0]
@@ -161,54 +209,34 @@ func filterTestSuites(suites *[]controllers.TestSuiteDesc, suiteArgument string)
continue
}
if testName == "" {
return &[]controllers.TestSuiteDesc{suite}
return &[]tests.TestSuiteDesc{suite}
}
// Only run a particular test in a suite
for _, test := range suite.Tests {
if test.Name != testName {
continue
}
return &[]controllers.TestSuiteDesc{
return &[]tests.TestSuiteDesc{
{
Name: suite.Name,
Tests: []controllers.TestDesc{test},
Tests: []tests.TestDesc{test},
},
}
}
errorf("Couldn't find test %s in suite %s", testName, suiteName)
utils.Logger.Errorf("Couldn't find test %s in suite %s", testName, suiteName)
}
errorf("Couldn't find test suite %s", suiteName)
utils.Logger.Errorf("Couldn't find test suite %s", suiteName)
return nil
}
func checkTestRunner() {
testRunnerFound := false
for _, module := range revel.Modules {
if module.ImportPath == revel.Config.StringDefault("module.testrunner", "github.com/revel/modules/testrunner") {
testRunnerFound = true
break
}
}
if !testRunnerFound {
errorf(`Error: The testrunner module is not running.
You can add it to a run mode configuration with the following line:
module.testrunner = github.com/revel/modules/testrunner
`)
}
}
// Get a list of tests from server.
// Since this is the first request to the server, retry/sleep a couple times
// in case it hasn't finished starting up yet.
func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) {
func getTestsList(baseURL string) (*[]tests.TestSuiteDesc, error) {
var (
err error
err error
resp *http.Response
testSuites []controllers.TestSuiteDesc
testSuites []tests.TestSuiteDesc
)
for i := 0; ; i++ {
if resp, err = http.Get(baseURL + "/@tests.list"); err == nil {
@@ -221,9 +249,9 @@ func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) {
continue
}
if err != nil {
errorf("Failed to request test list: %s", err)
utils.Logger.Fatalf("Failed to request test list: %s %s", baseURL, err)
} else {
errorf("Failed to request test list: non-200 response")
utils.Logger.Fatalf("Failed to request test list: non-200 response %s", baseURL)
}
}
defer func() {
@@ -235,21 +263,15 @@ func getTestsList(baseURL string) (*[]controllers.TestSuiteDesc, error) {
return &testSuites, err
}
func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSuiteDesc) (*[]controllers.TestSuiteResult, bool) {
// Load the result template, which we execute for each suite.
module, _ := revel.ModuleByName("testrunner")
TemplateLoader := revel.NewTemplateLoader([]string{filepath.Join(module.Path, "app", "views")})
if err := TemplateLoader.Refresh(); err != nil {
errorf("Failed to compile templates: %s", err)
}
resultTemplate, err := TemplateLoader.Template("TestRunner/SuiteResult.html")
if err != nil {
errorf("Failed to load suite result template: %s", err)
}
// Run the testsuites using the container
func runTestSuites(paths *model.RevelContainer, baseURL, resultPath string, testSuites *[]tests.TestSuiteDesc) (*[]tests.TestSuiteResult, bool) {
// We can determine the testsuite location by finding the test module and extracting the data from it
resultFilePath := filepath.Join(paths.ModulePathMap["testrunner"].Path, "app", "views", "TestRunner/SuiteResult.html")
var (
overallSuccess = true
failedResults []controllers.TestSuiteResult
failedResults []tests.TestSuiteResult
)
for _, suite := range *testSuites {
// Print the name of the suite we're running.
@@ -261,21 +283,25 @@ func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSui
// Run every test.
startTime := time.Now()
suiteResult := controllers.TestSuiteResult{Name: suite.Name, Passed: true}
suiteResult := tests.TestSuiteResult{Name: suite.Name, Passed: true}
for _, test := range suite.Tests {
testURL := baseURL + "/@tests/" + suite.Name + "/" + test.Name
resp, err := http.Get(testURL)
if err != nil {
errorf("Failed to fetch test result at url %s: %s", testURL, err)
utils.Logger.Errorf("Failed to fetch test result at url %s: %s", testURL, err)
}
defer func() {
_ = resp.Body.Close()
}()
var testResult controllers.TestResult
var testResult tests.TestResult
err = json.NewDecoder(resp.Body).Decode(&testResult)
if err == nil && !testResult.Passed {
suiteResult.Passed = false
utils.Logger.Error("Test Failed", "suite", suite.Name, "test", test.Name)
fmt.Printf(" %s.%s : FAILED\n", suite.Name, test.Name)
} else {
fmt.Printf(" %s.%s : PASSED\n", suite.Name, test.Name)
}
suiteResult.Results = append(suiteResult.Results, testResult)
}
@@ -291,12 +317,8 @@ func runTestSuites(baseURL, resultPath string, testSuites *[]controllers.TestSui
// Create the result HTML file.
suiteResultFilename := filepath.Join(resultPath,
fmt.Sprintf("%s.%s.html", suite.Name, strings.ToLower(suiteResultStr)))
suiteResultFile, err := os.Create(suiteResultFilename)
if err != nil {
errorf("Failed to create result file %s: %s", suiteResultFilename, err)
}
if err = resultTemplate.Render(suiteResultFile, suiteResult); err != nil {
errorf("Failed to render result template: %s", err)
if err := utils.RenderTemplate(suiteResultFilename, resultFilePath, suiteResult); err != nil {
utils.Logger.Error("Failed to render template", "error", err)
}
}

31
revel/test_test.go Normal file
View File

@@ -0,0 +1,31 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
// test the commands
func TestRevelTest(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-test", a)
t.Run("Test", func(t *testing.T) {
a := assert.New(t)
c := newApp("test-test", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "Failed to run test-test")
c.Index = model.TEST
c.Test.ImportPath = c.ImportPath
a.Nil(main.Commands[model.TEST].RunWith(c), "Failed to run test-test")
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil {
a.Fail("Failed to remove test path")
}
}
}

View File

@@ -1,176 +0,0 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package main
import (
"archive/tar"
"compress/gzip"
"fmt"
"go/build"
"io"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/revel/revel"
)
// LoggedError is wrapper to differentiate logged panics from unexpected ones.
type LoggedError struct{ error }
func panicOnError(err error, msg string) {
if revErr, ok := err.(*revel.Error); (ok && revErr != nil) || (!ok && err != nil) {
fmt.Fprintf(os.Stderr, "Abort: %s: %s\n", msg, err)
panic(LoggedError{err})
}
}
func mustCopyFile(destFilename, srcFilename string) {
destFile, err := os.Create(destFilename)
panicOnError(err, "Failed to create file "+destFilename)
srcFile, err := os.Open(srcFilename)
panicOnError(err, "Failed to open file "+srcFilename)
_, err = io.Copy(destFile, srcFile)
panicOnError(err,
fmt.Sprintf("Failed to copy data from %s to %s", srcFile.Name(), destFile.Name()))
err = destFile.Close()
panicOnError(err, "Failed to close file "+destFile.Name())
err = srcFile.Close()
panicOnError(err, "Failed to close file "+srcFile.Name())
}
func mustRenderTemplate(destPath, srcPath string, data map[string]interface{}) {
tmpl, err := template.ParseFiles(srcPath)
panicOnError(err, "Failed to parse template "+srcPath)
f, err := os.Create(destPath)
panicOnError(err, "Failed to create "+destPath)
err = tmpl.Execute(f, data)
panicOnError(err, "Failed to render template "+srcPath)
err = f.Close()
panicOnError(err, "Failed to close "+f.Name())
}
func mustChmod(filename string, mode os.FileMode) {
err := os.Chmod(filename, mode)
panicOnError(err, fmt.Sprintf("Failed to chmod %d %q", mode, filename))
}
// copyDir copies a directory tree over to a new directory. Any files ending in
// ".template" are treated as a Go template and rendered using the given data.
// Additionally, the trailing ".template" is stripped from the file name.
// Also, dot files and dot directories are skipped.
func mustCopyDir(destDir, srcDir string, data map[string]interface{}) error {
return revel.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
// Get the relative path from the source base, and the corresponding path in
// the dest directory.
relSrcPath := strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator))
destPath := filepath.Join(destDir, relSrcPath)
// Skip dot files and dot directories.
if strings.HasPrefix(relSrcPath, ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Create a subdirectory if necessary.
if info.IsDir() {
err := os.MkdirAll(filepath.Join(destDir, relSrcPath), 0777)
if !os.IsExist(err) {
panicOnError(err, "Failed to create directory")
}
return nil
}
// If this file ends in ".template", render it as a template.
if strings.HasSuffix(relSrcPath, ".template") {
mustRenderTemplate(destPath[:len(destPath)-len(".template")], srcPath, data)
return nil
}
// Else, just copy it over.
mustCopyFile(destPath, srcPath)
return nil
})
}
func mustTarGzDir(destFilename, srcDir string) string {
zipFile, err := os.Create(destFilename)
panicOnError(err, "Failed to create archive")
defer func() {
_ = zipFile.Close()
}()
gzipWriter := gzip.NewWriter(zipFile)
defer func() {
_ = gzipWriter.Close()
}()
tarWriter := tar.NewWriter(gzipWriter)
defer func() {
_ = tarWriter.Close()
}()
_ = revel.Walk(srcDir, func(srcPath string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
srcFile, err := os.Open(srcPath)
panicOnError(err, "Failed to read source file")
defer func() {
_ = srcFile.Close()
}()
err = tarWriter.WriteHeader(&tar.Header{
Name: strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)),
Size: info.Size(),
Mode: int64(info.Mode()),
ModTime: info.ModTime(),
})
panicOnError(err, "Failed to write tar entry header")
_, err = io.Copy(tarWriter, srcFile)
panicOnError(err, "Failed to copy")
return nil
})
return zipFile.Name()
}
func exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
// empty returns true if the given directory is empty.
// the directory must exist.
func empty(dirname string) bool {
dir, err := os.Open(dirname)
if err != nil {
errorf("error opening directory: %s", err)
}
defer func() {
_ = dir.Close()
}()
results, _ := dir.Readdir(1)
return len(results) == 0
}
func importPathFromCurrentDir() string {
pwd, _ := os.Getwd()
importPath, _ := filepath.Rel(filepath.Join(build.Default.GOPATH, "src"), pwd)
return filepath.ToSlash(importPath)
}

View File

@@ -10,29 +10,239 @@ package main
import (
"fmt"
"runtime"
"github.com/revel/revel"
"github.com/revel/cmd"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"bytes"
)
type (
// The version container
VersionCommand struct {
Command *model.CommandConfig // The command
revelVersion *model.Version // The Revel framework version
modulesVersion *model.Version // The Revel modules version
cmdVersion *model.Version // The tool version
}
)
var cmdVersion = &Command{
UsageLine: "version",
UsageLine: "revel version",
Short: "displays the Revel Framework and Go version",
Long: `
Displays the Revel Framework and Go version.
For example:
revel version
revel version [<application path>]
`,
}
func init() {
cmdVersion.Run = versionApp
v := &VersionCommand{}
cmdVersion.UpdateConfig = v.UpdateConfig
cmdVersion.RunWith = v.RunWith
}
func versionApp(args []string) {
fmt.Printf("Version(s):")
fmt.Printf("\n Revel v%v (%v)", revel.Version, revel.BuildDate)
fmt.Printf("\n %s %s/%s\n\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
// Update the version
func (v *VersionCommand) UpdateConfig(c *model.CommandConfig, args []string) bool {
if len(args) > 0 {
c.Version.ImportPath = args[0]
}
return true
}
// Displays the version of go and Revel
func (v *VersionCommand) RunWith(c *model.CommandConfig) (err error) {
utils.Logger.Info("Requesting version information", "config", c)
v.Command = c
// Update the versions with the local values
v.updateLocalVersions()
needsUpdates := true
versionInfo := ""
for x := 0; x < 2 && needsUpdates; x++ {
needsUpdates = false
versionInfo, needsUpdates = v.doRepoCheck(x == 0)
}
fmt.Printf("%s\n\nGo Location:%s\n\n", versionInfo, c.GoCmd)
cmd := exec.Command(c.GoCmd, "version")
cmd.Stdout = os.Stdout
if e := cmd.Start(); e != nil {
fmt.Println("Go command error ", e)
} else {
cmd.Wait()
}
return
}
// Checks the Revel repos for the latest version
func (v *VersionCommand) doRepoCheck(updateLibs bool) (versionInfo string, needsUpdate bool) {
var (
title string
localVersion *model.Version
)
for _, repo := range []string{"revel", "cmd", "modules"} {
versonFromRepo, err := v.versionFromRepo(repo, "", "version.go")
if err != nil {
utils.Logger.Info("Failed to get version from repo", "repo", repo, "error", err)
}
switch repo {
case "revel":
title, repo, localVersion = "Revel Framework", "github.com/revel/revel", v.revelVersion
case "cmd":
title, repo, localVersion = "Revel Cmd", "github.com/revel/cmd/revel", v.cmdVersion
case "modules":
title, repo, localVersion = "Revel Modules", "github.com/revel/modules", v.modulesVersion
}
// Only do an update on the first loop, and if specified to update
versionInfo = versionInfo + v.outputVersion(title, repo, localVersion, versonFromRepo)
}
return
}
// Checks for updates if needed
func (v *VersionCommand) doUpdate(title, repo string, local, remote *model.Version) {
utils.Logger.Info("Updating package", "package", title, "repo", repo)
fmt.Println("Attempting to update package", title)
if err := v.Command.PackageResolver(repo); err != nil {
utils.Logger.Error("Unable to update repo", "repo", repo, "error", err)
} else if repo == "github.com/revel/cmd/revel" {
// One extra step required here to run the install for the command
utils.Logger.Fatal("Revel command tool was updated, you must manually run the following command before continuing\ngo install github.com/revel/cmd/revel")
}
return
}
// Prints out the local and remote versions, calls update if needed
func (v *VersionCommand) outputVersion(title, repo string, local, remote *model.Version) (output string) {
buffer := &bytes.Buffer{}
remoteVersion := "Unknown"
if remote != nil {
remoteVersion = remote.VersionString()
}
localVersion := "Unknown"
if local != nil {
localVersion = local.VersionString()
}
fmt.Fprintf(buffer, "%s\t:\t%s\t(%s remote master branch)\n", title, localVersion, remoteVersion)
return buffer.String()
}
// Returns the version from the repository
func (v *VersionCommand) versionFromRepo(repoName, branchName, fileName string) (version *model.Version, err error) {
if branchName == "" {
branchName = "master"
}
// Try to download the version of file from the repo, just use an http connection to retrieve the source
// Assuming that the repo is github
fullurl := "https://raw.githubusercontent.com/revel/" + repoName + "/" + branchName + "/" + fileName
resp, err := http.Get(fullurl)
if err != nil {
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
utils.Logger.Info("Got version file", "from", fullurl, "content", string(body))
return v.versionFromBytes(body)
}
// Returns version information from a file called version on the gopath
func (v *VersionCommand) compareAndUpdateVersion(remoteVersion *model.Version, localVersion *model.Version) (err error) {
return
}
func (v *VersionCommand) versionFromFilepath(sourcePath string) (version *model.Version, err error) {
utils.Logger.Info("Fullpath to revel", "dir", sourcePath)
sourceStream, err := ioutil.ReadFile(filepath.Join(sourcePath, "version.go"))
if err != nil {
return
}
return v.versionFromBytes(sourceStream)
}
// Returns version information from a file called version on the gopath
func (v *VersionCommand) versionFromBytes(sourceStream []byte) (version *model.Version, err error) {
fset := token.NewFileSet() // positions are relative to fset
// Parse src but stop after processing the imports.
f, err := parser.ParseFile(fset, "", sourceStream, parser.ParseComments)
if err != nil {
err = utils.NewBuildError("Failed to parse Revel version error:", "error", err)
return
}
version = &model.Version{}
// Print the imports from the file's AST.
for _, s := range f.Decls {
genDecl, ok := s.(*ast.GenDecl)
if !ok {
continue
}
if genDecl.Tok != token.CONST {
continue
}
for _, a := range genDecl.Specs {
spec := a.(*ast.ValueSpec)
r := spec.Values[0].(*ast.BasicLit)
switch spec.Names[0].Name {
case "Version":
version.ParseVersion(strings.Replace(r.Value, `"`, "", -1))
case "BuildDate":
version.BuildDate = r.Value
case "MinimumGoVersion":
version.MinGoVersion = r.Value
}
}
}
return
}
// Fetch the local version of revel from the file system
func (v *VersionCommand) updateLocalVersions() {
v.cmdVersion = &model.Version{}
v.cmdVersion.ParseVersion(cmd.Version)
v.cmdVersion.BuildDate = cmd.BuildDate
v.cmdVersion.MinGoVersion = cmd.MinimumGoVersion
if v.Command.Version.ImportPath=="" {
return
}
pathMap, err := utils.FindSrcPaths(v.Command.AppPath, []string{model.RevelImportPath, model.RevelModulesImportPath}, v.Command.PackageResolver)
if err != nil {
utils.Logger.Warn("Unable to extract version information from Revel library", "path", pathMap[model.RevelImportPath], "error", err)
return
}
utils.Logger.Info("Fullpath to revel modules", "dir", pathMap[model.RevelImportPath])
v.revelVersion, err = v.versionFromFilepath(pathMap[model.RevelImportPath])
if err != nil {
utils.Logger.Warn("Unable to extract version information from Revel", "error,err")
}
v.modulesVersion, err = v.versionFromFilepath(pathMap[model.RevelModulesImportPath])
if err != nil {
utils.Logger.Warn("Unable to extract version information from Revel Modules", "path", pathMap[model.RevelModulesImportPath], "error", err)
}
return
}

41
revel/version_test.go Normal file
View File

@@ -0,0 +1,41 @@
package main_test
import (
"github.com/revel/cmd/model"
"github.com/revel/cmd/revel"
"github.com/stretchr/testify/assert"
"os"
"path/filepath"
"testing"
)
// test the commands
func TestVersion(t *testing.T) {
a := assert.New(t)
gopath := setup("revel-test-version", a)
t.Run("Version", func(t *testing.T) {
a := assert.New(t)
c := newApp("version-test", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "Check new")
c.Build.ImportPath = c.ImportPath
c.Build.TargetPath = filepath.Join(gopath, "build-test", "target")
a.Nil(main.Commands[model.BUILD].RunWith(c), "Failed to run build")
c.Index = model.VERSION
c.Version.ImportPath = c.ImportPath
a.Nil(main.Commands[model.VERSION].RunWith(c), "Failed to run version-test")
})
t.Run("Version-Nobuild", func(t *testing.T) {
a := assert.New(t)
c := newApp("version-test2", model.NEW, nil, a)
a.Nil(main.Commands[model.NEW].RunWith(c), "Check new")
c.Index = model.VERSION
c.Version.ImportPath = c.ImportPath
a.Nil(main.Commands[model.VERSION].RunWith(c), "Failed to run version-test")
})
if !t.Failed() {
if err := os.RemoveAll(gopath); err != nil && err!=os.ErrNotExist {
a.Fail("Failed to remove test path",err.Error())
}
}
}

156
tests/testrunner.go Normal file
View File

@@ -0,0 +1,156 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package tests
import (
"fmt"
"html/template"
"reflect"
"strings"
"github.com/revel/cmd/utils"
)
// TestSuiteDesc is used for storing information about a single test suite.
// This structure is required by revel test cmd.
type TestSuiteDesc struct {
Name string
Tests []TestDesc
// Elem is reflect.Type which can be used for accessing methods
// of the test suite.
Elem reflect.Type
}
// TestDesc is used for describing a single test of some test suite.
// This structure is required by revel test cmd.
type TestDesc struct {
Name string
}
// TestSuiteResult stores the results the whole test suite.
// This structure is required by revel test cmd.
type TestSuiteResult struct {
Name string
Passed bool
Results []TestResult
}
// TestResult represents the results of running a single test of some test suite.
// This structure is required by revel test cmd.
type TestResult struct {
Name string
Passed bool
ErrorHTML template.HTML
ErrorSummary string
}
var (
testSuites []TestSuiteDesc // A list of all available tests.
none = []reflect.Value{} // It is used as input for reflect call in a few places.
// registeredTests simplifies the search of test suites by their name.
// "TestSuite.TestName" is used as a key. Value represents index in testSuites.
registeredTests map[string]int
)
/*
Below are helper functions.
*/
// describeSuite expects testsuite interface as input parameter
// and returns its description in a form of TestSuiteDesc structure.
func describeSuite(testSuite interface{}) TestSuiteDesc {
t := reflect.TypeOf(testSuite)
// Get a list of methods of the embedded test type.
// It will be used to make sure the same tests are not included in multiple test suites.
super := t.Elem().Field(0).Type
superMethods := map[string]bool{}
for i := 0; i < super.NumMethod(); i++ {
// Save the current method's name.
superMethods[super.Method(i).Name] = true
}
// Get a list of methods on the test suite that take no parameters, return
// no results, and were not part of the embedded type's method set.
var tests []TestDesc
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
mt := m.Type
// Make sure the test method meets the criterias:
// - method of testSuite without input parameters;
// - nothing is returned;
// - has "Test" prefix;
// - doesn't belong to the embedded structure.
methodWithoutParams := (mt.NumIn() == 1 && mt.In(0) == t)
nothingReturned := (mt.NumOut() == 0)
hasTestPrefix := (strings.HasPrefix(m.Name, "Test"))
if methodWithoutParams && nothingReturned && hasTestPrefix && !superMethods[m.Name] {
// Register the test suite's index so we can quickly find it by test's name later.
registeredTests[t.Elem().Name()+"."+m.Name] = len(testSuites)
// Add test to the list of tests.
tests = append(tests, TestDesc{m.Name})
}
}
return TestSuiteDesc{
Name: t.Elem().Name(),
Tests: tests,
Elem: t.Elem(),
}
}
// errorSummary gets an error and returns its summary in human readable format.
func errorSummary(err *utils.SourceError) (message string) {
expectedPrefix := "(expected)"
actualPrefix := "(actual)"
errDesc := err.Description
//strip the actual/expected stuff to provide more condensed display.
if strings.Index(errDesc, expectedPrefix) == 0 {
errDesc = errDesc[len(expectedPrefix):]
}
if strings.LastIndex(errDesc, actualPrefix) > 0 {
errDesc = errDesc[0 : len(errDesc)-len(actualPrefix)]
}
errFile := err.Path
slashIdx := strings.LastIndex(errFile, "/")
if slashIdx > 0 {
errFile = errFile[slashIdx+1:]
}
message = fmt.Sprintf("%s %s#%d", errDesc, errFile, err.Line)
/*
// If line of error isn't known return the message as is.
if err.Line == 0 {
return
}
// Otherwise, include info about the line number and the relevant
// source code lines.
message += fmt.Sprintf(" (around line %d): ", err.Line)
for _, line := range err.ContextSource() {
if line.IsError {
message += line.Source
}
}
*/
return
}
//sortbySuiteName sorts the testsuites by name.
type sortBySuiteName []interface{}
func (a sortBySuiteName) Len() int { return len(a) }
func (a sortBySuiteName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortBySuiteName) Less(i, j int) bool {
return reflect.TypeOf(a[i]).Elem().Name() < reflect.TypeOf(a[j]).Elem().Name()
}

104
utils/build_error.go Normal file
View File

@@ -0,0 +1,104 @@
package utils
import (
"fmt"
"github.com/revel/cmd/logger"
"strconv"
"regexp"
)
type (
BuildError struct {
Stack interface{}
Message string
Args []interface{}
}
)
// Returns a new builed error
func NewBuildError(message string, args ...interface{}) (b *BuildError) {
Logger.Info(message, args...)
b = &BuildError{}
b.Message = message
b.Args = args
b.Stack = logger.NewCallStack()
Logger.Info("Stack", "stack", b.Stack)
return b
}
// Returns a new BuildError if err is not nil
func NewBuildIfError(err error, message string, args ...interface{}) (b error) {
if err != nil {
if berr, ok := err.(*BuildError); ok {
// This is already a build error so just append the args
berr.Args = append(berr.Args, args...)
return berr
} else {
args = append(args, "error", err.Error())
b = NewBuildError(message, args...)
}
}
return
}
// BuildError implements Error() string
func (b *BuildError) Error() string {
return fmt.Sprint(b.Message, b.Args)
}
// Parse the output of the "go build" command.
// Return a detailed Error.
func NewCompileError(importPath, errorLink string, error error) *SourceError {
// Get the stack from the error
errorMatch := regexp.MustCompile(`(?m)^([^:#]+):(\d+):(\d+:)? (.*)$`).
FindSubmatch([]byte(error.Error()))
if errorMatch == nil {
errorMatch = regexp.MustCompile(`(?m)^(.*?):(\d+):\s(.*?)$`).FindSubmatch([]byte(error.Error()))
if errorMatch == nil {
Logger.Error("Failed to parse build errors", "error", error)
return &SourceError{
SourceType: "Go code",
Title: "Go Compilation Error",
Description: "See console for build error.",
}
}
errorMatch = append(errorMatch, errorMatch[3])
Logger.Error("Build errors", "errors", error)
}
// Read the source for the offending file.
var (
relFilename = string(errorMatch[1]) // e.g. "src/revel/sample/app/controllers/app.go"
absFilename = relFilename
line, _ = strconv.Atoi(string(errorMatch[2]))
description = string(errorMatch[4])
compileError = &SourceError{
SourceType: "Go code",
Title: "Go Compilation Error",
Path: relFilename,
Description: description,
Line: line,
}
)
// errorLink := paths.Config.StringDefault("error.link", "")
if errorLink != "" {
compileError.SetLink(errorLink)
}
fileStr, err := ReadLines(absFilename)
if err != nil {
compileError.MetaError = absFilename + ": " + err.Error()
Logger.Info("Unable to readlines " + compileError.MetaError, "error", err)
return compileError
}
compileError.SourceLines = fileStr
return compileError
}

36
utils/command.go Normal file
View File

@@ -0,0 +1,36 @@
package utils
import (
"go/build"
"os"
"os/exec"
"strings"
"bytes"
"path/filepath"
)
// Initialize the command based on the GO environment
func CmdInit(c *exec.Cmd, addGoPath bool, basePath string) {
c.Dir = basePath
// Dep does not like paths that are not real, convert all paths in go to real paths
realPath := &bytes.Buffer{}
if addGoPath {
for _, p := range filepath.SplitList(build.Default.GOPATH) {
rp, _ := filepath.EvalSymlinks(p)
if realPath.Len() > 0 {
realPath.WriteString(string(filepath.ListSeparator))
}
realPath.WriteString(rp)
}
// Go 1.8 fails if we do not include the GOROOT
c.Env = []string{"GOPATH=" + realPath.String(), "GOROOT=" + os.Getenv("GOROOT")}
}
// Fetch the rest of the env variables
for _, e := range os.Environ() {
pair := strings.Split(e, "=")
if pair[0] == "GOPATH" || pair[0] == "GOROOT" {
continue
}
c.Env = append(c.Env, e)
}
}

91
utils/error.go Normal file
View File

@@ -0,0 +1,91 @@
package utils
import (
"fmt"
"strconv"
"strings"
)
// The error is a wrapper for the
type (
SourceError struct {
SourceType string // The type of source that failed to build.
Title, Path, Description string // Description of the error, as presented to the user.
Line, Column int // Where the error was encountered.
SourceLines []string // The entire source file, split into lines.
Stack string // The raw stack trace string from debug.Stack().
MetaError string // Error that occurred producing the error page.
Link string // A configurable link to wrap the error source in
}
SourceLine struct {
Source string
Line int
IsError bool
}
)
// Return a new error object
func NewError(source, title, path, description string) *SourceError {
return &SourceError{
SourceType:source,
Title:title,
Path:path,
Description:description,
}
}
// Creates a link based on the configuration setting "errors.link"
func (e *SourceError) SetLink(errorLink string) {
errorLink = strings.Replace(errorLink, "{{Path}}", e.Path, -1)
errorLink = strings.Replace(errorLink, "{{Line}}", strconv.Itoa(e.Line), -1)
e.Link = "<a href=" + errorLink + ">" + e.Path + ":" + strconv.Itoa(e.Line) + "</a>"
}
// Error method constructs a plaintext version of the error, taking
// account that fields are optionally set. Returns e.g. Compilation Error
// (in views/header.html:51): expected right delim in end; got "}"
func (e *SourceError) Error() string {
if e == nil {
panic("opps")
}
loc := ""
if e.Path != "" {
line := ""
if e.Line != 0 {
line = fmt.Sprintf(":%d", e.Line)
}
loc = fmt.Sprintf("(in %s%s)", e.Path, line)
}
header := loc
if e.Title != "" {
if loc != "" {
header = fmt.Sprintf("%s %s: ", e.Title, loc)
} else {
header = fmt.Sprintf("%s: ", e.Title)
}
}
return fmt.Sprintf("%s%s", header, e.Description)
}
// ContextSource method returns a snippet of the source around
// where the error occurred.
func (e *SourceError) ContextSource() []SourceLine {
if e.SourceLines == nil {
return nil
}
start := (e.Line - 1) - 5
if start < 0 {
start = 0
}
end := (e.Line - 1) + 5
if end > len(e.SourceLines) {
end = len(e.SourceLines)
}
lines := make([]SourceLine, end - start)
for i, src := range e.SourceLines[start:end] {
fileLine := start + i + 1
lines[i] = SourceLine{src, fileLine, fileLine == e.Line}
}
return lines
}

395
utils/file.go Normal file
View File

@@ -0,0 +1,395 @@
package utils
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"errors"
"html/template"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"golang.org/x/tools/go/packages"
)
// DirExists returns true if the given path exists and is a directory.
func DirExists(filename string) bool {
fileInfo, err := os.Stat(filename)
return err == nil && fileInfo.IsDir()
}
// MustReadLines reads the lines of the given file. Panics in the case of error.
func MustReadLines(filename string) []string {
r, err := ReadLines(filename)
if err != nil {
panic(err)
}
return r
}
// ReadLines reads the lines of the given file. Panics in the case of error.
func ReadLines(filename string) ([]string, error) {
dataBytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return strings.Split(string(dataBytes), "\n"), nil
}
// Copy file returns error
func CopyFile(destFilename, srcFilename string) (err error) {
destFile, err := os.Create(destFilename)
if err != nil {
return NewBuildIfError(err, "Failed to create file", "file", destFilename)
}
srcFile, err := os.Open(srcFilename)
if err != nil {
return NewBuildIfError(err, "Failed to open file", "file", srcFilename)
}
_, err = io.Copy(destFile, srcFile)
if err != nil {
return NewBuildIfError(err, "Failed to copy data", "fromfile", srcFilename, "tofile", destFilename)
}
err = destFile.Close()
if err != nil {
return NewBuildIfError(err, "Failed to close file", "file", destFilename)
}
err = srcFile.Close()
if err != nil {
return NewBuildIfError(err, "Failed to close file", "file", srcFilename)
}
return
}
// GenerateTemplate renders the given template to produce source code, which it writes
// to the given file.
func GenerateTemplate(filename, templateSource string, args map[string]interface{}) (err error) {
tmpl := template.Must(template.New("").Parse(templateSource))
var b bytes.Buffer
if err = tmpl.Execute(&b, args); err != nil {
return NewBuildIfError(err, "ExecuteTemplate: Execute failed")
}
sourceCode := b.String()
filePath := filepath.Dir(filename)
if !DirExists(filePath) {
err = os.MkdirAll(filePath, 0777)
if err != nil && !os.IsExist(err) {
return NewBuildIfError(err, "Failed to make directory", "dir", filePath)
}
}
// Create the file
file, err := os.Create(filename)
if err != nil {
Logger.Fatal("Failed to create file", "error", err)
return
}
defer func() {
_ = file.Close()
}()
if _, err = file.WriteString(sourceCode); err != nil {
Logger.Fatal("Failed to write to file: ", "error", err)
}
return
}
// Given the target path and source path and data. A template
func RenderTemplate(destPath, srcPath string, data interface{}) (err error) {
tmpl, err := template.ParseFiles(srcPath)
if err != nil {
return NewBuildIfError(err, "Failed to parse template " + srcPath)
}
f, err := os.Create(destPath)
if err != nil {
return NewBuildIfError(err, "Failed to create ", "path", destPath)
}
err = tmpl.Execute(f, data)
if err != nil {
return NewBuildIfError(err, "Failed to Render template " + srcPath)
}
err = f.Close()
if err != nil {
return NewBuildIfError(err, "Failed to close file stream " + destPath)
}
return
}
// Given the target path and source path and data. A template
func RenderTemplateToStream(output io.Writer, srcPath []string, data interface{}) (err error) {
tmpl, err := template.ParseFiles(srcPath...)
if err != nil {
return NewBuildIfError(err, "Failed to parse template " + srcPath[0])
}
err = tmpl.Execute(output, data)
if err != nil {
return NewBuildIfError(err, "Failed to render template " + srcPath[0])
}
return
}
func MustChmod(filename string, mode os.FileMode) {
err := os.Chmod(filename, mode)
PanicOnError(err, fmt.Sprintf("Failed to chmod %d %q", mode, filename))
}
// Called if panic
func PanicOnError(err error, msg string) {
if revErr, ok := err.(*SourceError); (ok && revErr != nil) || (!ok && err != nil) {
Logger.Panicf("Abort: %s: %s %s", msg, revErr, err)
}
}
// copyDir copies a directory tree over to a new directory. Any files ending in
// ".template" are treated as a Go template and rendered using the given data.
// Additionally, the trailing ".template" is stripped from the file name.
// Also, dot files and dot directories are skipped.
func CopyDir(destDir, srcDir string, data map[string]interface{}) error {
if !DirExists(srcDir) {
return nil
}
return fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error {
// Get the relative path from the source base, and the corresponding path in
// the dest directory.
relSrcPath := strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator))
destPath := filepath.Join(destDir, relSrcPath)
// Skip dot files and dot directories.
if strings.HasPrefix(relSrcPath, ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
// Create a subdirectory if necessary.
if info.IsDir() {
err := os.MkdirAll(filepath.Join(destDir, relSrcPath), 0777)
if !os.IsExist(err) {
return NewBuildIfError(err, "Failed to create directory", "path", destDir + "/" + relSrcPath)
}
return nil
}
// If this file ends in ".template", render it as a template.
if strings.HasSuffix(relSrcPath, ".template") {
return RenderTemplate(destPath[:len(destPath) - len(".template")], srcPath, data)
}
// Else, just copy it over.
return CopyFile(destPath, srcPath)
})
}
// Shortcut to fsWalk
func Walk(root string, walkFn filepath.WalkFunc) error {
return fsWalk(root, root, walkFn)
}
// Walk the path tree using the function
// Every file found will call the function
func fsWalk(fname string, linkName string, walkFn filepath.WalkFunc) error {
fsWalkFunc := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
var name string
name, err = filepath.Rel(fname, path)
if err != nil {
return err
}
path = filepath.Join(linkName, name)
if err == nil && info.Mode() & os.ModeSymlink == os.ModeSymlink {
var symlinkPath string
symlinkPath, err = filepath.EvalSymlinks(path)
if err != nil {
return err
}
// https://github.com/golang/go/blob/master/src/path/filepath/path.go#L392
info, err = os.Lstat(symlinkPath)
if err != nil {
return walkFn(path, info, err)
}
if info.IsDir() {
return fsWalk(symlinkPath, path, walkFn)
}
}
return walkFn(path, info, err)
}
err := filepath.Walk(fname, fsWalkFunc)
return err
}
// Tar gz the folder
func TarGzDir(destFilename, srcDir string) (name string, err error) {
zipFile, err := os.Create(destFilename)
if err != nil {
return "", NewBuildIfError(err, "Failed to create archive", "file", destFilename)
}
defer func() {
_ = zipFile.Close()
}()
gzipWriter := gzip.NewWriter(zipFile)
defer func() {
_ = gzipWriter.Close()
}()
tarWriter := tar.NewWriter(gzipWriter)
defer func() {
_ = tarWriter.Close()
}()
err = fsWalk(srcDir, srcDir, func(srcPath string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
srcFile, err := os.Open(srcPath)
if err != nil {
return NewBuildIfError(err, "Failed to read file", "file", srcPath)
}
defer func() {
_ = srcFile.Close()
}()
err = tarWriter.WriteHeader(&tar.Header{
Name: strings.TrimLeft(srcPath[len(srcDir):], string(os.PathSeparator)),
Size: info.Size(),
Mode: int64(info.Mode()),
ModTime: info.ModTime(),
})
if err != nil {
return NewBuildIfError(err, "Failed to write tar entry header", "file", srcPath)
}
_, err = io.Copy(tarWriter, srcFile)
if err != nil {
return NewBuildIfError(err, "Failed to copy file", "file", srcPath)
}
return nil
})
return zipFile.Name(), err
}
// Return true if the file exists
func Exists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
// empty returns true if the given directory is empty.
// the directory must exist.
func Empty(dirname string) bool {
if !DirExists(dirname) {
return true
}
dir, err := os.Open(dirname)
if err != nil {
Logger.Infof("error opening directory: %s", err)
return false
}
defer func() {
_ = dir.Close()
}()
results, _ := dir.Readdir(1)
return len(results) == 0
}
// Find the full source dir for the import path, uses the build.Default.GOPATH to search for the directory
func FindSrcPaths(appPath string, packageList []string, packageResolver func(pkgName string) error) (sourcePathsmap map[string]string, err error) {
sourcePathsmap, missingList, err := findSrcPaths(appPath, packageList)
if err != nil && packageResolver != nil || len(missingList) > 0 {
Logger.Info("Failed to find package, attempting to call resolver for missing packages", "missing packages", missingList)
for _, item := range missingList {
if err = packageResolver(item); err != nil {
return
}
}
sourcePathsmap, missingList, err = findSrcPaths(appPath, packageList)
}
if err != nil && len(missingList) > 0 {
for _, missing := range missingList {
Logger.Error("Unable to import this package", "package", missing)
}
}
return
}
var NO_APP_FOUND = errors.New("No app found")
var NO_REVEL_FOUND = errors.New("No revel found")
// Find the full source dir for the import path, uses the build.Default.GOPATH to search for the directory
func findSrcPaths(appPath string, packagesList []string) (sourcePathsmap map[string]string, missingList[] string, err error) {
// Use packages to fetch
// by not specifying env, we will use the default env
config := &packages.Config{
Mode: packages.NeedName | packages.NeedFiles,
Dir:appPath,
}
sourcePathsmap = map[string]string{}
Logger.Infof("Environment path %s root %s config env %s", os.Getenv("GOPATH"), os.Getenv("GOROOT"), config.Env)
pkgs, err := packages.Load(config, packagesList...)
Logger.Infof("Environment path %s root %s config env %s", os.Getenv("GOPATH"), os.Getenv("GOROOT"), config.Env)
Logger.Info("Loaded packages ", "len results", len(pkgs), "error", err, "basedir", appPath)
for _, packageName := range packagesList {
found := false
log := Logger.New("seeking", packageName)
for _, pck := range pkgs {
log.Info("Found package", "package", pck.ID)
if pck.ID == packageName {
if pck.Errors != nil && len(pck.Errors) > 0 {
log.Error("Error ", "count", len(pck.Errors), "App Import Path", pck.ID, "filesystem path", pck.PkgPath, "errors", pck.Errors)
// continue
}
//a,_ := pck.MarshalJSON()
log.Info("Found ", "count", len(pck.GoFiles), "App Import Path", pck.ID, "apppath", appPath)
if len(pck.GoFiles) > 0 {
sourcePathsmap[packageName] = filepath.Dir(pck.GoFiles[0])
found = true
}
}
}
if !found {
if packageName == "github.com/revel/revel" {
err = NO_REVEL_FOUND
} else {
err = NO_APP_FOUND
}
missingList = append(missingList, packageName)
}
}
return
}

48
utils/log.go Normal file
View File

@@ -0,0 +1,48 @@
package utils
import (
"fmt"
"github.com/revel/cmd/logger"
"github.com/revel/config"
"os"
"strings"
)
var Logger = logger.New()
func InitLogger(basePath string, logLevel logger.LogLevel) {
newContext := config.NewContext()
if logLevel == logger.LvlDebug {
newContext.SetOption("log.debug.output", "stdout")
println("Debug on")
} else {
newContext.SetOption("log.debug.output", "off")
}
if logLevel >= logger.LvlInfo {
newContext.SetOption("log.info.output", "stdout")
} else {
newContext.SetOption("log.inf.output", "off")
}
newContext.SetOption("log.warn.output", "stderr")
newContext.SetOption("log.error.output", "stderr")
newContext.SetOption("log.crit.output", "stderr")
Logger.SetHandler(logger.InitializeFromConfig(basePath, newContext))
}
// This function is to throw a panic that may be caught by the packger so it can perform the needed
// imports
func Retry(format string, args ...interface{}) {
// Ensure the user's command prompt starts on the next line.
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
fmt.Fprintf(os.Stderr, format, args...)
panic(format) // Panic instead of os.Exit so that deferred will run.
}
type LoggedError struct{ error }
func NewLoggedError(err error) *LoggedError {
return &LoggedError{err}
}

11
utils/strings.go Normal file
View File

@@ -0,0 +1,11 @@
package utils
// Return true if the target string is in the list
func ContainsString(list []string, target string) bool {
for _, el := range list {
if el == target {
return true
}
}
return false
}

16
version.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (c) 2012-2018 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package cmd
const (
// Version current Revel version
Version = "1.0.0"
// BuildDate latest commit/release date
BuildDate = "2020-07-11"
// MinimumGoVersion minimum required Go version for Revel
MinimumGoVersion = ">= go1.12"
)

295
watcher/watcher.go Normal file
View File

@@ -0,0 +1,295 @@
// Copyright (c) 2012-2016 The Revel Framework Authors, All rights reserved.
// Revel Framework source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
package watcher
import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/revel/cmd/model"
"github.com/revel/cmd/utils"
"github.com/fsnotify/fsnotify"
"time"
)
// Listener is an interface for receivers of filesystem events.
type Listener interface {
// Refresh is invoked by the watcher on relevant filesystem events.
// If the listener returns an error, it is served to the user on the current request.
Refresh() *utils.SourceError
}
// DiscerningListener allows the receiver to selectively watch files.
type DiscerningListener interface {
Listener
WatchDir(info os.FileInfo) bool
WatchFile(basename string) bool
}
// Watcher allows listeners to register to be notified of changes under a given
// directory.
type Watcher struct {
// Parallel arrays of watcher/listener pairs.
watchers []*fsnotify.Watcher
listeners []Listener
forceRefresh bool
eagerRefresh bool
serial bool
lastError int
notifyMutex sync.Mutex
paths *model.RevelContainer
refreshTimer *time.Timer // The timer to countdown the next refresh
timerMutex *sync.Mutex // A mutex to prevent concurrent updates
refreshChannel chan *utils.SourceError
refreshChannelCount int
refreshTimerMS time.Duration // The number of milliseconds between refreshing builds
}
// Creates a new watched based on the container
func NewWatcher(paths *model.RevelContainer, eagerRefresh bool) *Watcher {
return &Watcher{
forceRefresh: false,
lastError: -1,
paths: paths,
refreshTimerMS: time.Duration(paths.Config.IntDefault("watch.rebuild.delay", 1000)),
eagerRefresh: eagerRefresh ||
paths.DevMode &&
paths.Config.BoolDefault("watch", true) &&
paths.Config.StringDefault("watch.mode", "normal") == "eager",
timerMutex: &sync.Mutex{},
refreshChannel: make(chan *utils.SourceError, 10),
refreshChannelCount: 0,
}
}
// Listen registers for events within the given root directories (recursively).
func (w *Watcher) Listen(listener Listener, roots ...string) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
utils.Logger.Fatal("Watcher: Failed to create watcher", "error", err)
}
// Replace the unbuffered Event channel with a buffered one.
// Otherwise multiple change events only come out one at a time, across
// multiple page views. (There appears no way to "pump" the events out of
// the watcher)
// This causes a notification when you do a check in go, since you are modifying a buffer in use
watcher.Events = make(chan fsnotify.Event, 100)
watcher.Errors = make(chan error, 10)
// Walk through all files / directories under the root, adding each to watcher.
for _, p := range roots {
// is the directory / file a symlink?
f, err := os.Lstat(p)
if err == nil && f.Mode() & os.ModeSymlink == os.ModeSymlink {
var realPath string
realPath, err = filepath.EvalSymlinks(p)
if err != nil {
panic(err)
}
p = realPath
}
fi, err := os.Stat(p)
if err != nil {
utils.Logger.Fatal("Watcher: Failed to stat watched path", "path", p, "error", err)
continue
}
// If it is a file, watch that specific file.
if !fi.IsDir() {
err = watcher.Add(p)
if err != nil {
utils.Logger.Fatal("Watcher: Failed to watch", "path", p, "error", err)
}
continue
}
var watcherWalker func(path string, info os.FileInfo, err error) error
watcherWalker = func(path string, info os.FileInfo, err error) error {
if err != nil {
utils.Logger.Fatal("Watcher: Error walking path:", "error", err)
return nil
}
if info.IsDir() {
if dl, ok := listener.(DiscerningListener); ok {
if !dl.WatchDir(info) {
return filepath.SkipDir
}
}
err = watcher.Add(path)
if err != nil {
utils.Logger.Fatal("Watcher: Failed to watch", "path", path, "error", err)
}
}
return nil
}
// Else, walk the directory tree.
err = utils.Walk(p, watcherWalker)
if err != nil {
utils.Logger.Fatal("Watcher: Failed to walk directory", "path", p, "error", err)
}
}
if w.eagerRefresh {
// Create goroutine to notify file changes in real time
go w.NotifyWhenUpdated(listener, watcher)
}
w.watchers = append(w.watchers, watcher)
w.listeners = append(w.listeners, listener)
}
// NotifyWhenUpdated notifies the watcher when a file event is received.
func (w *Watcher) NotifyWhenUpdated(listener Listener, watcher *fsnotify.Watcher) {
for {
select {
case ev := <-watcher.Events:
if w.rebuildRequired(ev, listener) {
if w.serial {
// Serialize listener.Refresh() calls.
w.notifyMutex.Lock()
if err := listener.Refresh(); err != nil {
utils.Logger.Error("Watcher: Listener refresh reported error:", "error", err)
}
w.notifyMutex.Unlock()
} else {
// Run refresh in parallel
go func() {
w.notifyInProcess(listener)
}()
}
}
case <-watcher.Errors:
continue
}
}
}
// Notify causes the watcher to forward any change events to listeners.
// It returns the first (if any) error returned.
func (w *Watcher) Notify() *utils.SourceError {
if w.serial {
// Serialize Notify() calls.
w.notifyMutex.Lock()
defer w.notifyMutex.Unlock()
}
for i, watcher := range w.watchers {
listener := w.listeners[i]
// Pull all pending events / errors from the watcher.
refresh := false
for {
select {
case ev := <-watcher.Events:
if w.rebuildRequired(ev, listener) {
refresh = true
}
continue
case <-watcher.Errors:
continue
default:
// No events left to pull
}
break
}
utils.Logger.Info("Watcher:Notify refresh state", "Current Index", i, " last error index", w.lastError,
"force", w.forceRefresh, "refresh", refresh, "lastError", w.lastError == i)
if w.forceRefresh || refresh || w.lastError == i {
var err *utils.SourceError
if w.serial {
err = listener.Refresh()
} else {
err = w.notifyInProcess(listener)
}
if err != nil {
w.lastError = i
w.forceRefresh = true
return err
} else {
w.lastError = -1
w.forceRefresh = false
}
}
}
return nil
}
// Build a queue for refresh notifications
// this will not return until one of the queue completes
func (w *Watcher) notifyInProcess(listener Listener) (err *utils.SourceError) {
shouldReturn := false
// This code block ensures that either a timer is created
// or that a process would be added the the h.refreshChannel
func() {
w.timerMutex.Lock()
defer w.timerMutex.Unlock()
// If we are in the process of a rebuild, forceRefresh will always be true
w.forceRefresh = true
if w.refreshTimer != nil {
utils.Logger.Info("Found existing timer running, resetting")
w.refreshTimer.Reset(time.Millisecond * w.refreshTimerMS)
shouldReturn = true
w.refreshChannelCount++
} else {
w.refreshTimer = time.NewTimer(time.Millisecond * w.refreshTimerMS)
}
}()
// If another process is already waiting for the timer this one
// only needs to return the output from the channel
if shouldReturn {
return <-w.refreshChannel
}
utils.Logger.Info("Waiting for refresh timer to expire")
<-w.refreshTimer.C
w.timerMutex.Lock()
// Ensure the queue is properly dispatched even if a panic occurs
defer func() {
for x := 0; x < w.refreshChannelCount; x++ {
w.refreshChannel <- err
}
w.refreshChannelCount = 0
w.refreshTimer = nil
w.timerMutex.Unlock()
}()
err = listener.Refresh()
if err != nil {
utils.Logger.Info("Watcher: Recording error last build, setting rebuild on", "error", err)
} else {
w.lastError = -1
w.forceRefresh = false
}
utils.Logger.Info("Rebuilt, result", "error", err)
return
}
func (w *Watcher) rebuildRequired(ev fsnotify.Event, listener Listener) bool {
// Ignore changes to dotfiles.
if strings.HasPrefix(filepath.Base(ev.Name), ".") {
return false
}
if dl, ok := listener.(DiscerningListener); ok {
if !dl.WatchFile(ev.Name) || ev.Op & fsnotify.Chmod == fsnotify.Chmod {
return false
}
}
return true
}