From 8c18b506154dd9a281194b3542079254f25003b3 Mon Sep 17 00:00:00 2001 From: Jinkun Jang Date: Wed, 13 Mar 2013 02:19:52 +0900 Subject: [PATCH] Tizen 2.1 base --- LICENSE.APLv2 | 202 ++ NOTICE | 4 +- README | 4 +- build-cli | 320 +- build-svr | 215 +- dibs-web/Gemfile | 50 + dibs-web/Gemfile.lock | 118 + dibs-web/README.rdoc | 261 ++ dibs-web/Rakefile | 7 + dibs-web/app/assets/images/rails.png | Bin 0 -> 6646 bytes dibs-web/app/controllers/admin_controller.rb | 79 + .../controllers/admin_distribution_controller.rb | 261 ++ dibs-web/app/controllers/admin_group_controller.rb | 240 ++ .../app/controllers/admin_project_controller.rb | 301 ++ .../app/controllers/admin_server_controller.rb | 424 +++ dibs-web/app/controllers/admin_user_controller.rb | 142 + dibs-web/app/controllers/application_controller.rb | 162 + dibs-web/app/controllers/jobs_controller.rb | 634 ++++ dibs-web/app/controllers/projects_controller.rb | 407 +++ dibs-web/app/controllers/sessions_controller.rb | 107 + dibs-web/app/controllers/users_controller.rb | 172 ++ dibs-web/app/controllers/utils.rb | 130 + dibs-web/app/helpers/admin_distribution_helper.rb | 2 + dibs-web/app/helpers/admin_group_helper.rb | 2 + dibs-web/app/helpers/admin_helper.rb | 2 + dibs-web/app/helpers/admin_project_helper.rb | 2 + dibs-web/app/helpers/admin_server_helper.rb | 2 + dibs-web/app/helpers/admin_user_helper.rb | 2 + dibs-web/app/helpers/application_helper.rb | 2 + dibs-web/app/helpers/jobs_helper.rb | 2 + dibs-web/app/helpers/projects_helper.rb | 4 + dibs-web/app/helpers/sessions_helper.rb | 2 + dibs-web/app/helpers/users_helper.rb | 2 + dibs-web/app/mailers/.gitkeep | 0 dibs-web/app/models/.gitkeep | 0 dibs-web/app/models/distribution.rb | 3 + dibs-web/app/models/group.rb | 5 + dibs-web/app/models/group_project_access.rb | 4 + dibs-web/app/models/job.rb | 3 + dibs-web/app/models/os_category.rb | 4 + dibs-web/app/models/package.rb | 3 + dibs-web/app/models/project.rb | 3 + dibs-web/app/models/project_bin.rb | 3 + dibs-web/app/models/project_git.rb | 3 + dibs-web/app/models/project_os.rb | 4 + dibs-web/app/models/remote_build_server.rb | 3 + dibs-web/app/models/server_config.rb | 3 + dibs-web/app/models/source.rb | 3 + dibs-web/app/models/supported_os.rb | 3 + dibs-web/app/models/sync_pkg_server.rb | 3 + dibs-web/app/models/sync_project.rb | 3 + dibs-web/app/models/user.rb | 29 + dibs-web/app/models/user_group.rb | 7 + dibs-web/config.ru | 4 + dibs-web/config/application.rb | 62 + dibs-web/config/boot.rb | 6 + dibs-web/config/database.yml | 46 + dibs-web/config/environment.rb | 7 + dibs-web/config/environments/development.rb | 37 + dibs-web/config/environments/production.rb | 67 + dibs-web/config/environments/test.rb | 37 + .../config/initializers/backtrace_silencers.rb | 7 + dibs-web/config/initializers/inflections.rb | 15 + dibs-web/config/initializers/mime_types.rb | 5 + dibs-web/config/initializers/secret_token.rb | 7 + dibs-web/config/initializers/session_store.rb | 8 + dibs-web/config/initializers/wrap_parameters.rb | 14 + dibs-web/config/locales/en.yml | 5 + dibs-web/config/routes.rb | 179 ++ dibs-web/db/schema.rb | 76 + dibs-web/db/seeds.rb | 7 + dibs-web/doc/README_FOR_APP | 2 + dibs-web/public/.project | 57 + dibs-web/public/404.html | 26 + dibs-web/public/422.html | 26 + dibs-web/public/500.html | 25 + dibs-web/public/config.xml | 7 + dibs-web/public/favicon.ico | 0 dibs-web/public/index.html | 1017 +++++++ .../public/javascripts/admin-distribution-add.js | 62 + .../javascripts/admin-distribution-modify.js | 110 + dibs-web/public/javascripts/admin-distribution.js | 130 + dibs-web/public/javascripts/admin-group-add.js | 101 + dibs-web/public/javascripts/admin-group-modify.js | 129 + dibs-web/public/javascripts/admin-group.js | 165 ++ dibs-web/public/javascripts/admin-project-add.js | 177 ++ .../public/javascripts/admin-project-modify.js | 209 ++ dibs-web/public/javascripts/admin-project.js | 255 ++ dibs-web/public/javascripts/admin-server-add.js | 133 + dibs-web/public/javascripts/admin-server-modify.js | 125 + dibs-web/public/javascripts/admin-server-remove.js | 116 + dibs-web/public/javascripts/admin-server.js | 109 + dibs-web/public/javascripts/admin-user-modify.js | 71 + dibs-web/public/javascripts/admin-user.js | 119 + dibs-web/public/javascripts/application.js | 15 + dibs-web/public/javascripts/build.js | 344 +++ dibs-web/public/javascripts/dibs-api.js | 392 +++ dibs-web/public/javascripts/jobs.js | 695 +++++ dibs-web/public/javascripts/log.js | 184 ++ dibs-web/public/javascripts/main.js | 337 +++ dibs-web/public/javascripts/popup-window.js | 90 + dibs-web/public/javascripts/post-process.js | 35 + dibs-web/public/javascripts/projects.js | 178 ++ dibs-web/public/javascripts/session.js | 146 + dibs-web/public/javascripts/user.js | 136 + dibs-web/public/log.html | 114 + dibs-web/public/robots.txt | 5 + dibs-web/public/stylesheets/application.css | 13 + dibs-web/public/stylesheets/images/ajax-loader.gif | Bin 0 -> 7825 bytes dibs-web/public/stylesheets/images/ajax-loader.png | Bin 0 -> 340 bytes .../public/stylesheets/images/icons-18-black.png | Bin 0 -> 1767 bytes .../public/stylesheets/images/icons-18-white.png | Bin 0 -> 1806 bytes .../public/stylesheets/images/icons-36-black.png | Bin 0 -> 3611 bytes .../public/stylesheets/images/icons-36-white.png | Bin 0 -> 3648 bytes dibs-web/public/upload.html | 97 + dibs-web/script/rails | 6 + doc/DIBS_Advanced_Guide.pdf | Bin 0 -> 502611 bytes doc/Tizen_SDK_Development_Guide.pdf | Bin 0 -> 168703 bytes doc/Tizen_SDK_Package_Guide.pdf | Bin 0 -> 56301 bytes package/build.linux | 16 +- package/build.macos | 26 + package/build.windows | 26 + package/changelog | 109 + package/pkginfo.manifest | 15 +- package/pkginfo.manifest.local | 6 + pkg-build | 48 +- pkg-clean | 24 +- pkg-cli | 188 +- pkg-svr | 81 +- src/build_server/BinaryUploadProject.rb | 200 ++ src/build_server/BuildClientOptionParser.rb | 256 +- src/build_server/BuildJob.rb | 1222 ++++++-- src/build_server/BuildServer.rb | 922 ++++-- src/build_server/BuildServerController.rb | 873 +++++- src/build_server/BuildServerException.rb | 31 + src/build_server/BuildServerOptionParser.rb | 432 ++- src/build_server/CommonJob.rb | 277 ++ src/build_server/CommonProject.rb | 355 +++ src/build_server/DistributionManager.rb | 227 ++ src/build_server/GitBuildJob.rb | 336 ++- src/build_server/GitBuildProject.rb | 154 + src/build_server/JobClean.rb | 199 ++ src/build_server/JobLog.rb | 77 +- src/build_server/JobManager.rb | 589 ++++ src/build_server/MultiBuildJob.rb | 456 +++ src/build_server/PackageSync.rb | 238 ++ src/build_server/ProjectManager.rb | 364 +++ src/build_server/RegisterPackageJob.rb | 549 ++++ src/build_server/RemoteBuildJob.rb | 9 +- src/build_server/RemoteBuildServer.rb | 248 ++ src/build_server/RemoteBuilder.rb | 259 ++ src/build_server/ReverseBuildChecker.rb | 217 ++ src/build_server/SocketJobRequestListener.rb | 1016 ++++++- src/builder/Builder.rb | 746 +++-- src/builder/CleanOptionParser.rb | 42 +- src/builder/optionparser.rb | 78 +- src/common/Action.rb | 47 + src/common/BuildComm.rb | 657 +++++ src/common/FileTransferViaDirect.rb | 138 + src/common/FileTransferViaFTP.rb | 288 ++ src/common/PackageManifest.rb | 101 +- src/common/ScheduledActionHandler.rb | 117 + src/common/Version.rb | 29 +- src/common/dependency.rb | 46 +- src/common/execute_with_log.rb | 56 + src/common/log.rb | 84 +- src/common/mail.rb | 50 +- src/common/package.rb | 217 +- src/common/parser.rb | 497 ++-- src/common/utils.rb | 678 ++++- src/pkg_server/DistSync.rb | 97 + src/pkg_server/SocketRegisterListener.rb | 240 ++ src/pkg_server/client.rb | 3106 +++++++++++--------- src/pkg_server/clientOptParser.rb | 299 +- src/pkg_server/distribution.rb | 1167 +++++--- src/pkg_server/downloader.rb | 72 +- src/pkg_server/installer.rb | 721 +++-- src/pkg_server/packageServer.rb | 912 +++--- src/pkg_server/packageServerConfig.rb | 34 + src/pkg_server/packageServerLog.rb | 12 +- src/pkg_server/serverOptParser.rb | 339 ++- test/bin/bin_0.0.0_linux.zip | Bin 0 -> 620 bytes test/bin/bin_0.0.0_ubuntu-32.zip | Bin 0 -> 624 bytes test/bin/bin_0.0.1_linux.zip | Bin 0 -> 622 bytes test/bin/bin_0.0.1_ubuntu-32.zip | Bin 0 -> 626 bytes test/bin/src.tar.gz | Bin 0 -> 150 bytes test/build-server.basic1/build-cli-01.testcase | 43 + test/build-server.basic1/build-cli-02.testcase | 29 + test/build-server.basic1/build-cli-03.testcase | 30 + test/build-server.basic1/build-cli-03_1.testcase | 28 + test/build-server.basic1/build-cli-04.testcase | 6 + test/build-server.basic1/build-cli-05.testcase | 6 + test/build-server.basic1/build-cli-06.testcase | 6 + test/build-server.basic1/build-cli-07.testcase | 11 + test/build-server.basic1/build-cli-08.testcase | 32 + test/build-server.basic1/build-cli-09.testcase | 19 + test/build-server.basic1/build-cli-10.testcase | 32 + test/build-server.basic1/build-cli-11.testcase | 11 + test/build-server.basic1/build-cli-12.testcase | 11 + test/build-server.basic1/build-cli-12_1.testcase | 10 + test/build-server.basic1/build-cli-13.testcase | 38 + test/build-server.basic1/build-cli-14.testcase | 8 + test/build-server.basic1/build-cli-15.testcase | 8 + test/build-server.basic1/build-cli-16.testcase | 8 + test/build-server.basic1/build-cli-17.testcase | 40 + test/build-server.basic1/build-cli-18.testcase | 31 + test/build-server.basic1/build-cli-19.testcase | 16 + test/build-server.basic1/build-cli-20.testcase | 6 + test/build-server.basic1/build-cli-21.testcase | 6 + test/build-server.basic1/build-cli-22.testcase | 16 + test/build-server.basic1/build-cli-23.testcase | 25 + test/build-server.basic1/build-cli-24.testcase | 12 + test/build-server.basic1/build-cli-25.testcase | 20 + test/build-server.basic1/build-cli-26.testcase | 45 + test/build-server.basic1/build-cli-27.testcase | 7 + test/build-server.basic1/build-cli-28.testcase | 33 + test/build-server.basic1/build-cli-29.testcase | 38 + test/build-server.basic1/build-cli-30.testcase | 7 + test/build-server.basic1/buildsvr.init | 58 + test/build-server.basic1/pkgsvr.init | 10 + test/build-server.basic1/testsuite | 31 + test/build-server.basic2/build-svr-01.testcase | 11 + test/build-server.basic2/build-svr-02.testcase | 79 + test/build-server.basic2/build-svr-03.testcase | 24 + test/build-server.basic2/build-svr-04.testcase | 12 + test/build-server.basic2/build-svr-05.testcase | 15 + test/build-server.basic2/build-svr-06.testcase | 11 + test/build-server.basic2/build-svr-07.testcase | 9 + test/build-server.basic2/build-svr-08.testcase | 12 + test/build-server.basic2/build-svr-09.testcase | 6 + test/build-server.basic2/build-svr-10.testcase | 6 + test/build-server.basic2/build-svr-11.testcase | 13 + test/build-server.basic2/build-svr-12.testcase | 6 + test/build-server.basic2/build-svr-13.testcase | 10 + test/build-server.basic2/build-svr-14.testcase | 14 + test/build-server.basic2/build-svr-15.testcase | 19 + test/build-server.basic2/build-svr-16.testcase | 15 + test/build-server.basic2/build-svr-17.testcase | 24 + test/build-server.basic2/build-svr-18.testcase | 14 + test/build-server.basic2/build-svr-19.testcase | 16 + test/build-server.basic2/build-svr-20.testcase | 22 + test/build-server.basic2/build-svr-21.testcase | 20 + test/build-server.basic2/testsuite | 19 + test/build-server.multi-svr1/01.testcase | 45 + test/build-server.multi-svr1/02.testcase | 46 + test/build-server.multi-svr1/buildsvr1.init | 46 + test/build-server.multi-svr1/buildsvr2.init | 26 + test/build-server.multi-svr1/pkgsvr.init | 10 + test/build-server.multi-svr1/testsuite | 2 + test/build-server.multi-svr2/01.testcase | 23 + test/build-server.multi-svr2/buildsvr.init | 37 + test/build-server.multi-svr2/pkgsvr1.init | 10 + test/build-server.multi-svr2/pkgsvr2.init | 10 + test/build-server.multi-svr2/testsuite | 1 + .../build-svr2-01.testcase | 23 + .../build-svr2-02.testcase | 7 + .../build-svr2-03.testcase | 18 + .../build-svr2-04.testcase | 19 + .../build-svr2-05.testcase | 6 + .../build-svr2-06.testcase | 66 + .../build-svr2-07.testcase | 78 + .../build-svr2-08.testcase | 21 + .../build-svr2-09.testcase | 12 + test/build-server.multi_dist1/testsuite | 9 + .../build-svr3-01.testcase | 14 + .../build-svr3-02.testcase | 56 + .../build-svr3-03.testcase | 28 + .../build-svr3-04.testcase | 18 + .../build-svr3-05.testcase | 46 + test/build-server.multi_dist2/buildsvr.init | 50 + test/build-server.multi_dist2/pkgsvr.init | 13 + test/build-server.multi_dist2/testsuite | 5 + test/git01/a.tar.gz | Bin 0 -> 14112 bytes test/git01/a1_v1.tar.gz | Bin 0 -> 9123 bytes test/git01/a_new.tar.gz | Bin 0 -> 16165 bytes test/git01/a_v1.tar.gz | Bin 0 -> 8555 bytes test/git01/a_v2.tar.gz | Bin 0 -> 9397 bytes test/git01/a_v3.tar.gz | Bin 0 -> 10099 bytes test/git01/a_v4.tar.gz | Bin 0 -> 10958 bytes test/git01/a_v5.tar.gz | Bin 0 -> 11639 bytes test/git01/b.tar.gz | Bin 0 -> 13951 bytes test/git01/b_new.tar.gz | Bin 0 -> 15255 bytes test/git01/b_v1.tar.gz | Bin 0 -> 8757 bytes test/git01/b_v2.tar.gz | Bin 0 -> 10258 bytes test/git01/b_v4.tar.gz | Bin 0 -> 11469 bytes test/git01/c.tar.gz | Bin 0 -> 18834 bytes test/git01/c_new.tar.gz | Bin 0 -> 19975 bytes test/git01/c_v1.tar.gz | Bin 0 -> 9107 bytes test/git01/c_v1_1.tar.gz | Bin 0 -> 9680 bytes test/git01/c_v2.tar.gz | Bin 0 -> 10857 bytes test/git01/c_v4.tar.gz | Bin 0 -> 11979 bytes test/git01/c_v5.tar.gz | Bin 0 -> 12861 bytes test/git01/d.tar.gz | Bin 0 -> 15469 bytes test/git01/d_v0.tar.gz | Bin 0 -> 17463 bytes test/packageserver.testsuite | 22 + test/packageserver01.testcase | 62 + test/packageserver02.testcase | 7 + test/packageserver03.testcase | 8 + test/packageserver04.testcase | 7 + test/packageserver05.testcase | 6 + test/packageserver06.testcase | 6 + test/packageserver07.testcase | 6 + test/packageserver08.testcase | 6 + test/packageserver09.testcase | 6 + test/packageserver10.testcase | 6 + test/packageserver11.testcase | 6 + test/packageserver12.testcase | 6 + test/packageserver13.testcase | 6 + test/packageserver14.testcase | 7 + test/packageserver15.testcase | 6 + test/packageserver16.testcase | 7 + test/packageserver17.testcase | 7 + test/packageserver18.testcase | 7 + test/packageserver19.testcase | 7 + test/packageserver20.testcase | 7 + test/packageserver21.testcase | 7 + test/packageserver22.testcase | 7 + test/packageserver23.testcase | 7 + test/packageserver24.testcase | 8 + test/packageserver25.testcase | 6 + test/pkg-cli-checkupgrade.testcase | 11 + test/pkg-cli-clean-f.testcase | 11 + test/pkg-cli-download-t.testcase | 13 + test/pkg-cli-download.testcase | 10 + test/pkg-cli-install-f.testcase | 11 + test/pkg-cli-install-t.testcase | 13 + test/pkg-cli-install.testcase | 10 + test/pkg-cli-installfile-f.testcase | 13 + test/pkg-cli-installfile.testcase | 12 + test/pkg-cli-listlpkg.testcase | 10 + test/pkg-cli-listrpkg.testcase | 6 + test/pkg-cli-showlpkg.testcase | 12 + test/pkg-cli-showrpkg.testcase | 8 + test/pkg-cli-source.testcase | 10 + test/pkg-cli-uninstall-t.testcase | 11 + test/pkg-cli-uninstall.testcase | 11 + test/pkg-cli-update.testcase | 6 + test/pkg-cli-upgrade.testcase | 11 + test/pkg-cli.testsuite | 18 + test/pkg-list | 34 +- test/pkg-list-local | 17 + test/pkginfo.manifest | 21 +- test/pkgsvr2.init | 7 + test/regression.rb | 203 ++ test/test.sh | 5 + test/test_pkglist_parser.rb | 9 +- test/test_server | 71 +- test/test_server_pkg_file/archive.zip | Bin 0 -> 308 bytes .../smart-build-interface_1.20.1_linux.zip | Bin 327 -> 308 bytes test/upgrade/01.testcase | 9 + test/upgrade/buildsvr1.init | 34 + test/upgrade/buildsvr2.init | 22 + test/upgrade/pkgsvr.init | 9 + test/upgrade/testsuite | 1 + tizen-ide/get_ide_sources.sh | 276 ++ upgrade | 285 ++ 356 files changed, 30058 insertions(+), 5069 deletions(-) create mode 100644 LICENSE.APLv2 create mode 100644 dibs-web/Gemfile create mode 100644 dibs-web/Gemfile.lock create mode 100644 dibs-web/README.rdoc create mode 100644 dibs-web/Rakefile create mode 100644 dibs-web/app/assets/images/rails.png create mode 100644 dibs-web/app/controllers/admin_controller.rb create mode 100644 dibs-web/app/controllers/admin_distribution_controller.rb create mode 100644 dibs-web/app/controllers/admin_group_controller.rb create mode 100644 dibs-web/app/controllers/admin_project_controller.rb create mode 100644 dibs-web/app/controllers/admin_server_controller.rb create mode 100644 dibs-web/app/controllers/admin_user_controller.rb create mode 100644 dibs-web/app/controllers/application_controller.rb create mode 100644 dibs-web/app/controllers/jobs_controller.rb create mode 100644 dibs-web/app/controllers/projects_controller.rb create mode 100644 dibs-web/app/controllers/sessions_controller.rb create mode 100644 dibs-web/app/controllers/users_controller.rb create mode 100644 dibs-web/app/controllers/utils.rb create mode 100644 dibs-web/app/helpers/admin_distribution_helper.rb create mode 100644 dibs-web/app/helpers/admin_group_helper.rb create mode 100644 dibs-web/app/helpers/admin_helper.rb create mode 100644 dibs-web/app/helpers/admin_project_helper.rb create mode 100644 dibs-web/app/helpers/admin_server_helper.rb create mode 100644 dibs-web/app/helpers/admin_user_helper.rb create mode 100644 dibs-web/app/helpers/application_helper.rb create mode 100644 dibs-web/app/helpers/jobs_helper.rb create mode 100644 dibs-web/app/helpers/projects_helper.rb create mode 100644 dibs-web/app/helpers/sessions_helper.rb create mode 100644 dibs-web/app/helpers/users_helper.rb create mode 100644 dibs-web/app/mailers/.gitkeep create mode 100644 dibs-web/app/models/.gitkeep create mode 100644 dibs-web/app/models/distribution.rb create mode 100644 dibs-web/app/models/group.rb create mode 100644 dibs-web/app/models/group_project_access.rb create mode 100644 dibs-web/app/models/job.rb create mode 100644 dibs-web/app/models/os_category.rb create mode 100644 dibs-web/app/models/package.rb create mode 100644 dibs-web/app/models/project.rb create mode 100644 dibs-web/app/models/project_bin.rb create mode 100644 dibs-web/app/models/project_git.rb create mode 100644 dibs-web/app/models/project_os.rb create mode 100644 dibs-web/app/models/remote_build_server.rb create mode 100644 dibs-web/app/models/server_config.rb create mode 100644 dibs-web/app/models/source.rb create mode 100644 dibs-web/app/models/supported_os.rb create mode 100644 dibs-web/app/models/sync_pkg_server.rb create mode 100644 dibs-web/app/models/sync_project.rb create mode 100644 dibs-web/app/models/user.rb create mode 100644 dibs-web/app/models/user_group.rb create mode 100644 dibs-web/config.ru create mode 100644 dibs-web/config/application.rb create mode 100644 dibs-web/config/boot.rb create mode 100644 dibs-web/config/database.yml create mode 100644 dibs-web/config/environment.rb create mode 100644 dibs-web/config/environments/development.rb create mode 100644 dibs-web/config/environments/production.rb create mode 100644 dibs-web/config/environments/test.rb create mode 100644 dibs-web/config/initializers/backtrace_silencers.rb create mode 100644 dibs-web/config/initializers/inflections.rb create mode 100644 dibs-web/config/initializers/mime_types.rb create mode 100644 dibs-web/config/initializers/secret_token.rb create mode 100644 dibs-web/config/initializers/session_store.rb create mode 100644 dibs-web/config/initializers/wrap_parameters.rb create mode 100644 dibs-web/config/locales/en.yml create mode 100644 dibs-web/config/routes.rb create mode 100644 dibs-web/db/schema.rb create mode 100644 dibs-web/db/seeds.rb create mode 100644 dibs-web/doc/README_FOR_APP create mode 100644 dibs-web/public/.project create mode 100644 dibs-web/public/404.html create mode 100644 dibs-web/public/422.html create mode 100644 dibs-web/public/500.html create mode 100644 dibs-web/public/config.xml create mode 100644 dibs-web/public/favicon.ico create mode 100644 dibs-web/public/index.html create mode 100644 dibs-web/public/javascripts/admin-distribution-add.js create mode 100644 dibs-web/public/javascripts/admin-distribution-modify.js create mode 100644 dibs-web/public/javascripts/admin-distribution.js create mode 100644 dibs-web/public/javascripts/admin-group-add.js create mode 100644 dibs-web/public/javascripts/admin-group-modify.js create mode 100644 dibs-web/public/javascripts/admin-group.js create mode 100644 dibs-web/public/javascripts/admin-project-add.js create mode 100644 dibs-web/public/javascripts/admin-project-modify.js create mode 100644 dibs-web/public/javascripts/admin-project.js create mode 100644 dibs-web/public/javascripts/admin-server-add.js create mode 100644 dibs-web/public/javascripts/admin-server-modify.js create mode 100644 dibs-web/public/javascripts/admin-server-remove.js create mode 100644 dibs-web/public/javascripts/admin-server.js create mode 100644 dibs-web/public/javascripts/admin-user-modify.js create mode 100644 dibs-web/public/javascripts/admin-user.js create mode 100644 dibs-web/public/javascripts/application.js create mode 100644 dibs-web/public/javascripts/build.js create mode 100644 dibs-web/public/javascripts/dibs-api.js create mode 100644 dibs-web/public/javascripts/jobs.js create mode 100644 dibs-web/public/javascripts/log.js create mode 100644 dibs-web/public/javascripts/main.js create mode 100644 dibs-web/public/javascripts/popup-window.js create mode 100644 dibs-web/public/javascripts/post-process.js create mode 100644 dibs-web/public/javascripts/projects.js create mode 100644 dibs-web/public/javascripts/session.js create mode 100644 dibs-web/public/javascripts/user.js create mode 100644 dibs-web/public/log.html create mode 100644 dibs-web/public/robots.txt create mode 100644 dibs-web/public/stylesheets/application.css create mode 100644 dibs-web/public/stylesheets/images/ajax-loader.gif create mode 100644 dibs-web/public/stylesheets/images/ajax-loader.png create mode 100644 dibs-web/public/stylesheets/images/icons-18-black.png create mode 100644 dibs-web/public/stylesheets/images/icons-18-white.png create mode 100644 dibs-web/public/stylesheets/images/icons-36-black.png create mode 100644 dibs-web/public/stylesheets/images/icons-36-white.png create mode 100644 dibs-web/public/upload.html create mode 100755 dibs-web/script/rails create mode 100644 doc/DIBS_Advanced_Guide.pdf create mode 100644 doc/Tizen_SDK_Development_Guide.pdf create mode 100644 doc/Tizen_SDK_Package_Guide.pdf create mode 100755 package/build.macos create mode 100755 package/build.windows create mode 100644 package/changelog create mode 100644 package/pkginfo.manifest.local create mode 100644 src/build_server/BinaryUploadProject.rb create mode 100644 src/build_server/BuildServerException.rb create mode 100644 src/build_server/CommonJob.rb create mode 100644 src/build_server/CommonProject.rb create mode 100644 src/build_server/DistributionManager.rb create mode 100644 src/build_server/GitBuildProject.rb create mode 100644 src/build_server/JobClean.rb create mode 100644 src/build_server/JobManager.rb create mode 100644 src/build_server/MultiBuildJob.rb create mode 100644 src/build_server/PackageSync.rb create mode 100644 src/build_server/ProjectManager.rb create mode 100644 src/build_server/RegisterPackageJob.rb create mode 100644 src/build_server/RemoteBuildServer.rb create mode 100644 src/build_server/RemoteBuilder.rb create mode 100644 src/build_server/ReverseBuildChecker.rb create mode 100644 src/common/Action.rb create mode 100644 src/common/BuildComm.rb create mode 100644 src/common/FileTransferViaDirect.rb create mode 100644 src/common/FileTransferViaFTP.rb create mode 100644 src/common/ScheduledActionHandler.rb create mode 100755 src/common/execute_with_log.rb create mode 100644 src/pkg_server/DistSync.rb create mode 100644 src/pkg_server/SocketRegisterListener.rb create mode 100644 src/pkg_server/packageServerConfig.rb create mode 100644 test/bin/bin_0.0.0_linux.zip create mode 100644 test/bin/bin_0.0.0_ubuntu-32.zip create mode 100644 test/bin/bin_0.0.1_linux.zip create mode 100644 test/bin/bin_0.0.1_ubuntu-32.zip create mode 100644 test/bin/src.tar.gz create mode 100644 test/build-server.basic1/build-cli-01.testcase create mode 100644 test/build-server.basic1/build-cli-02.testcase create mode 100644 test/build-server.basic1/build-cli-03.testcase create mode 100644 test/build-server.basic1/build-cli-03_1.testcase create mode 100644 test/build-server.basic1/build-cli-04.testcase create mode 100644 test/build-server.basic1/build-cli-05.testcase create mode 100644 test/build-server.basic1/build-cli-06.testcase create mode 100644 test/build-server.basic1/build-cli-07.testcase create mode 100644 test/build-server.basic1/build-cli-08.testcase create mode 100644 test/build-server.basic1/build-cli-09.testcase create mode 100644 test/build-server.basic1/build-cli-10.testcase create mode 100644 test/build-server.basic1/build-cli-11.testcase create mode 100644 test/build-server.basic1/build-cli-12.testcase create mode 100644 test/build-server.basic1/build-cli-12_1.testcase create mode 100644 test/build-server.basic1/build-cli-13.testcase create mode 100644 test/build-server.basic1/build-cli-14.testcase create mode 100644 test/build-server.basic1/build-cli-15.testcase create mode 100644 test/build-server.basic1/build-cli-16.testcase create mode 100644 test/build-server.basic1/build-cli-17.testcase create mode 100644 test/build-server.basic1/build-cli-18.testcase create mode 100644 test/build-server.basic1/build-cli-19.testcase create mode 100644 test/build-server.basic1/build-cli-20.testcase create mode 100644 test/build-server.basic1/build-cli-21.testcase create mode 100644 test/build-server.basic1/build-cli-22.testcase create mode 100644 test/build-server.basic1/build-cli-23.testcase create mode 100644 test/build-server.basic1/build-cli-24.testcase create mode 100644 test/build-server.basic1/build-cli-25.testcase create mode 100644 test/build-server.basic1/build-cli-26.testcase create mode 100644 test/build-server.basic1/build-cli-27.testcase create mode 100644 test/build-server.basic1/build-cli-28.testcase create mode 100644 test/build-server.basic1/build-cli-29.testcase create mode 100644 test/build-server.basic1/build-cli-30.testcase create mode 100755 test/build-server.basic1/buildsvr.init create mode 100755 test/build-server.basic1/pkgsvr.init create mode 100644 test/build-server.basic1/testsuite create mode 100644 test/build-server.basic2/build-svr-01.testcase create mode 100644 test/build-server.basic2/build-svr-02.testcase create mode 100644 test/build-server.basic2/build-svr-03.testcase create mode 100644 test/build-server.basic2/build-svr-04.testcase create mode 100644 test/build-server.basic2/build-svr-05.testcase create mode 100644 test/build-server.basic2/build-svr-06.testcase create mode 100644 test/build-server.basic2/build-svr-07.testcase create mode 100644 test/build-server.basic2/build-svr-08.testcase create mode 100644 test/build-server.basic2/build-svr-09.testcase create mode 100644 test/build-server.basic2/build-svr-10.testcase create mode 100644 test/build-server.basic2/build-svr-11.testcase create mode 100644 test/build-server.basic2/build-svr-12.testcase create mode 100644 test/build-server.basic2/build-svr-13.testcase create mode 100644 test/build-server.basic2/build-svr-14.testcase create mode 100644 test/build-server.basic2/build-svr-15.testcase create mode 100644 test/build-server.basic2/build-svr-16.testcase create mode 100644 test/build-server.basic2/build-svr-17.testcase create mode 100644 test/build-server.basic2/build-svr-18.testcase create mode 100644 test/build-server.basic2/build-svr-19.testcase create mode 100644 test/build-server.basic2/build-svr-20.testcase create mode 100644 test/build-server.basic2/build-svr-21.testcase create mode 100644 test/build-server.basic2/testsuite create mode 100644 test/build-server.multi-svr1/01.testcase create mode 100644 test/build-server.multi-svr1/02.testcase create mode 100755 test/build-server.multi-svr1/buildsvr1.init create mode 100755 test/build-server.multi-svr1/buildsvr2.init create mode 100755 test/build-server.multi-svr1/pkgsvr.init create mode 100644 test/build-server.multi-svr1/testsuite create mode 100644 test/build-server.multi-svr2/01.testcase create mode 100755 test/build-server.multi-svr2/buildsvr.init create mode 100755 test/build-server.multi-svr2/pkgsvr1.init create mode 100755 test/build-server.multi-svr2/pkgsvr2.init create mode 100644 test/build-server.multi-svr2/testsuite create mode 100644 test/build-server.multi_dist1/build-svr2-01.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-02.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-03.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-04.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-05.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-06.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-07.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-08.testcase create mode 100644 test/build-server.multi_dist1/build-svr2-09.testcase create mode 100644 test/build-server.multi_dist1/testsuite create mode 100644 test/build-server.multi_dist2/build-svr3-01.testcase create mode 100644 test/build-server.multi_dist2/build-svr3-02.testcase create mode 100644 test/build-server.multi_dist2/build-svr3-03.testcase create mode 100644 test/build-server.multi_dist2/build-svr3-04.testcase create mode 100644 test/build-server.multi_dist2/build-svr3-05.testcase create mode 100755 test/build-server.multi_dist2/buildsvr.init create mode 100755 test/build-server.multi_dist2/pkgsvr.init create mode 100644 test/build-server.multi_dist2/testsuite create mode 100644 test/git01/a.tar.gz create mode 100644 test/git01/a1_v1.tar.gz create mode 100644 test/git01/a_new.tar.gz create mode 100644 test/git01/a_v1.tar.gz create mode 100644 test/git01/a_v2.tar.gz create mode 100644 test/git01/a_v3.tar.gz create mode 100644 test/git01/a_v4.tar.gz create mode 100644 test/git01/a_v5.tar.gz create mode 100644 test/git01/b.tar.gz create mode 100644 test/git01/b_new.tar.gz create mode 100644 test/git01/b_v1.tar.gz create mode 100644 test/git01/b_v2.tar.gz create mode 100644 test/git01/b_v4.tar.gz create mode 100644 test/git01/c.tar.gz create mode 100644 test/git01/c_new.tar.gz create mode 100644 test/git01/c_v1.tar.gz create mode 100644 test/git01/c_v1_1.tar.gz create mode 100644 test/git01/c_v2.tar.gz create mode 100644 test/git01/c_v4.tar.gz create mode 100644 test/git01/c_v5.tar.gz create mode 100644 test/git01/d.tar.gz create mode 100644 test/git01/d_v0.tar.gz create mode 100644 test/packageserver.testsuite create mode 100644 test/packageserver01.testcase create mode 100644 test/packageserver02.testcase create mode 100644 test/packageserver03.testcase create mode 100644 test/packageserver04.testcase create mode 100644 test/packageserver05.testcase create mode 100644 test/packageserver06.testcase create mode 100644 test/packageserver07.testcase create mode 100644 test/packageserver08.testcase create mode 100644 test/packageserver09.testcase create mode 100644 test/packageserver10.testcase create mode 100644 test/packageserver11.testcase create mode 100644 test/packageserver12.testcase create mode 100644 test/packageserver13.testcase create mode 100644 test/packageserver14.testcase create mode 100644 test/packageserver15.testcase create mode 100644 test/packageserver16.testcase create mode 100644 test/packageserver17.testcase create mode 100644 test/packageserver18.testcase create mode 100644 test/packageserver19.testcase create mode 100644 test/packageserver20.testcase create mode 100644 test/packageserver21.testcase create mode 100644 test/packageserver22.testcase create mode 100644 test/packageserver23.testcase create mode 100644 test/packageserver24.testcase create mode 100644 test/packageserver25.testcase create mode 100644 test/pkg-cli-checkupgrade.testcase create mode 100644 test/pkg-cli-clean-f.testcase create mode 100644 test/pkg-cli-download-t.testcase create mode 100644 test/pkg-cli-download.testcase create mode 100644 test/pkg-cli-install-f.testcase create mode 100644 test/pkg-cli-install-t.testcase create mode 100644 test/pkg-cli-install.testcase create mode 100644 test/pkg-cli-installfile-f.testcase create mode 100644 test/pkg-cli-installfile.testcase create mode 100644 test/pkg-cli-listlpkg.testcase create mode 100644 test/pkg-cli-listrpkg.testcase create mode 100644 test/pkg-cli-showlpkg.testcase create mode 100644 test/pkg-cli-showrpkg.testcase create mode 100644 test/pkg-cli-source.testcase create mode 100644 test/pkg-cli-uninstall-t.testcase create mode 100644 test/pkg-cli-uninstall.testcase create mode 100644 test/pkg-cli-update.testcase create mode 100644 test/pkg-cli-upgrade.testcase create mode 100644 test/pkg-cli.testsuite create mode 100644 test/pkg-list-local create mode 100755 test/pkgsvr2.init create mode 100755 test/regression.rb create mode 100755 test/test.sh create mode 100644 test/test_server_pkg_file/archive.zip create mode 100644 test/upgrade/01.testcase create mode 100755 test/upgrade/buildsvr1.init create mode 100755 test/upgrade/buildsvr2.init create mode 100755 test/upgrade/pkgsvr.init create mode 100644 test/upgrade/testsuite create mode 100755 tizen-ide/get_ide_sources.sh create mode 100755 upgrade diff --git a/LICENSE.APLv2 b/LICENSE.APLv2 new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE.APLv2 @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE b/NOTICE index 4297ee3..901a81c 100644 --- a/NOTICE +++ b/NOTICE @@ -1 +1,3 @@ -Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2013 Samsung Electronics Co., Ltd. All rights reserved. +Except as noted, this software is licensed under Apache License, Version 2. +Please, see the LICENSE.APLv2 file for Apache License terms and conditions. diff --git a/README b/README index e54ec3f..f373da2 100644 --- a/README +++ b/README @@ -142,7 +142,7 @@ Building a SDK package is very simple. Here is the command for buiding package. ## pkg-build [-u ] [-o ] [-c ] [-r ] ## -u : Package server URL which contains binary and development packages. ## If ommited, it will use previous server URL. - ## -o : Target OS(linux or windows) + ## -o : Target OS(ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64) ## -c : Clean build"" ## If set, start build after downloading all dependent packages ## If not set, it will not download dependent packages if already downloaded @@ -195,7 +195,7 @@ There are more useful commands provided You can list up available packages of server. ## pkg-cli list-rpkg [-o ] [-u ] - ## -o : Target OS(linux or windows) + ## -o : Target OS(ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64) ## -u : Package server URL which contains binary and development packages. ## If ommited, it will use previous server URL. diff --git a/build-cli b/build-cli index 425c32a..fb9758e 100755 --- a/build-cli +++ b/build-cli @@ -1,7 +1,7 @@ -#!/usr/bin/ruby -d +#!/usr/bin/ruby =begin - + build-cli Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -35,75 +35,295 @@ $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/build_server" require "utils" require "BuildClientOptionParser" require "BuildComm" +require "FileTransferViaFTP" +require "FileTransferViaDirect" + -#option parsing -option = option_parse - -# if "--os" is not specified, use host os type -if option[:os].nil? then - option[:os] = Utils::HOST_OS -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end +#option parsing +begin + option = option_parse +rescue => e + puts e.message + exit 0 end -if option[:domain].nil? then - option[:domain] = "172.21.17.46" + +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + +def query( ip, port, sym ) + client = BuildCommClient.create( ip, port, nil, 0 ) + if client.nil? then + puts "Connection to server failed!" + return nil + end + client.send "QUERY|#{sym.strip}" + result = client.receive_data() + if result.nil? then + puts "Error: #{client.get_error_msg()}" + end + client.terminate + return result +end + +def query_system_info(ip, port) + # HOST SYSTEM INFO + puts "* SYSTEM INFO *" + data = query( ip, port, "SYSTEM") + if data.nil? then exit 1 end + + result = data[0].split(",").map { |x| x.strip } + puts "HOST-OS: #{result[0]}" + puts "MAX_WORKING_JOBS: #{result[1]}" + + # FTP INFO + puts "\n* FTP *" + data = query(ip, port, "FTP") + if data.nil? then exit 1 end + + result = data[0].split(",").map { |x| x.strip } + puts "FTP_ADDR: #{result[0]}" + puts "FTP_USERNAME: #{result[1]}" + + # SUPPORTED OS INFO + puts "\n* SUPPORTED OS LIST *" + data = query(ip, port, "OS") + if data.nil? then exit 1 end + + data.each do |item| + puts "#{item.strip}" + end + + # Friend lists + puts "\n* FRIEND SERVER LIST (WAIT|WORK/MAX) jobs [transfer count] *" + data = query(ip, port, "FRIEND") + if data.nil? then exit 1 end + i = 0 + data.each do |item| + i = i + 1 + info = item.split(",").map { |x| x.strip } + if info[0] == "DISCONNECTED" then + puts "#{i}. #{info[0]}" + else + puts "#{i}. #{info[0]} #{info[1]} server (#{info[2]}|#{info[3]}/#{info[4]}) [#{info[5]}]" + end + end end -if option[:port].nil? then - option[:port] = 2222 + +def query_project_list(ip, port) + puts "* PROJECT(S) *" + data = query( ip, port, "PROJECT") + data.each do |item| + tok = item.split(",").map { |x| x.strip } + type = (tok[0]=="G" ? "NORMAL":"REMOTE") + printf("%-25s %s %s\n",tok[1],type,tok[2]) + end +end + + +def query_job_list(ip, port) + puts "* JOB(S) *" + data = query(ip, port, "JOB") + data.each do |item| + tok = item.split(",").map { |x| x.strip } + if tok[4].nil? then + puts "#{tok[1]} #{tok[0]} #{tok[2]} #{tok[3]}" + else + puts "#{tok[1]} #{tok[0]} #{tok[2]} (#{tok[3]}) #{tok[4]}" + end + end +end + + +# if "--os" is not specified, use pe +if option[:os].nil? then + option[:os] = "default" +end + +if option[:domain].nil? then + puts "Warn: Build server IP address is not specified. 127.0.0.1 will be used" + option[:domain] = "127.0.0.1" end begin - case option[:cmd] - when "build" - client = BuildCommClient.create( option[:domain], option[:port]) + case option[:cmd] + when "build" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + client = BuildCommClient.create( result[0], result[1], nil, 0 ) if not client.nil? then - client.send "BUILD,GIT,#{option[:git]},#{option[:commit]},#{option[:os]},,#{option[:async]}" - client.print_stream + client.send "BUILD|GIT|#{option[:project]}|#{option[:passwd]}|#{option[:os]}|#{option[:async]}|#{option[:noreverse]}|#{option[:dist]}|#{option[:user]}|#{option[:verbose]}" + if not client.print_stream then + puts "ERROR: #{client.get_error_msg()}" + end + client.terminate + else + puts "Connection to server failed!" + exit 1 end when "resolve" - client = BuildCommClient.create( option[:domain], option[:port]) - if not client.nil? then - client.send "RESOLVE,GIT,#{option[:git]},#{option[:commit]},#{option[:os]},,#{option[:async]}" - client.print_stream - client.terminate + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 end - when "query" - # SYSTEM INFO - client = BuildCommClient.create( option[:domain], option[:port]) + client = BuildCommClient.create( result[0], result[1], nil, 0 ) if not client.nil? then - client.send "QUERY,SYSTEM" - result0 = client.receive_data() - if result0.nil? then - client.terminate - exit(-1) + client.send "RESOLVE|GIT|#{option[:project]}|#{option[:passwd]}|#{option[:os]}|#{option[:async]}|#{option[:dist]}|#{option[:user]}|#{option[:verbose]}" + if not client.print_stream then + puts "ERROR: #{client.get_error_msg()}" end - result0 = result0[0].split(",").map { |x| x.strip } - puts "HOST-OS: #{result0[0]}" - puts "MAX_WORKING_JOBS: #{result0[1]}" client.terminate end + when "query" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end - # JOB INFO - client = BuildCommClient.create( option[:domain], option[:port]) - if not client.nil? then - client.send "QUERY,JOB" - result1 = client.receive_data() - if result0.nil? then - client.terminate - exit(-1) + query_system_info( result[0], result[1] ) + puts "" + query_project_list( result[0], result[1]) + puts "" + query_job_list( result[0], result[1]) + + when "query-system" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_system_info( result[0], result[1] ) + + when "query-project" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_project_list( result[0], result[1]) + + when "query-job" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + + query_job_list( result[0], result[1] ) + + when "cancel" + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + if not option[:job].nil? then + client = BuildCommClient.create( result[0], result[1], nil, 0 ) + if not client.nil? then + client.send "CANCEL|#{option[:job]}|#{option[:passwd]}|#{option[:user]}" + result1 = client.receive_data() + if result1.nil? then + puts "Error: #{client.get_error_msg()}" + client.terminate + exit 1 + end + puts result1 + else + puts "Connection to server failed!" + exit 1 end - puts "* JOB *" - for item in result1 - tok = item.split(",").map { |x| x.strip } - puts "#{tok[1]} #{tok[0]} #{tok[2]}" + else + puts "you must input \"cancel job number\"!!" + exit 1 + end + + when "register" + # check file exist + if not File.exist? option[:package] then + puts "The file does not exist!.. #{option[:package]}" + exit(-1) + end + + result = Utils.parse_server_addr(option[:domain]) + if result.nil? then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + bs_ip = result[0] + bs_port = result[1] + + if not option[:fdomain].nil? then + ftp_result = Utils.parse_ftpserver_url(option[:fdomain]) + if ftp_result.nil? or ftp_result.length != 4 then + puts "FTP server url is incorrect. (#{option[:fdomain]})" + puts "Tune as following format." + puts " ftp://:@
" + exit 1 end + ip = ftp_result[0] + port = ftp_result[1] + username = ftp_result[2] + passwd = ftp_result[3] + transporter = FileTransferFTP.new( nil, ip, port, username, passwd ) + else + transporter = FileTransferDirect.new( nil ) + end + + # upload + client = BuildCommClient.create( bs_ip, bs_port, nil, 0 ) + if client.nil? then + puts "Can't access server #{bs_ip}:#{bs_port}" + exit(-1) + end + dock = Utils.create_uniq_name() + msg = "UPLOAD|#{dock}" + client.send( msg ) + result = client.send_file(option[:package], transporter) + client.terminate + if not result then + puts "Uploading file failed!.. #{option[:package]}" + exit(-1) + end + + # register + client = BuildCommClient.create( bs_ip, bs_port, nil, 0 ) + if client.nil? then + puts "Can't access server #{bs_ip}:#{bs_port}" + exit(-1) + end + client.send("REGISTER|BINARY|#{File.basename(option[:package])}|#{option[:passwd]}|#{dock}|#{option[:dist]}|#{option[:user]}|#{option[:noreverse]}") + if not client.print_stream then + puts "ERROR: #{client.get_error_msg()}" end + client.terminate else raise RuntimeError, "input option incorrect : #{option[:cmd]}" diff --git a/build-svr b/build-svr index f1ddaf9..fc9a98f 100755 --- a/build-svr +++ b/build-svr @@ -1,7 +1,7 @@ -#!/usr/bin/ruby -d +#!/usr/bin/ruby =begin - + build-svr Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -32,70 +32,191 @@ require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" $LOAD_PATH.unshift File.dirname(__FILE__)+"/src/build_server" require "utils" +require "log.rb" require "BuildServerOptionParser" require "BuildServerController" -#option parsing +#option parsing begin option = option_parse rescue => e - puts "Option parse error" puts e.message exit 0 end - -# if "--os" is not specified, use host os type -if option[:os].nil? then - host_os = `uname -s`.strip - case host_os - when "Linux" - option[:os] = "linux" - when /MINGW32.*/ - option[:os] = "windows" - when "Darwin" - option[:os] = "darwin" - else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end - end -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end -end - - +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end begin - case option[:cmd] - when "create" - BuildServerController.create_server( option[:name], Utils::WORKING_DIR, option[:url], option[:domain], option[:pid] ) + case option[:cmd] + when "create" + ftpsvr_addr = nil; ftpsvr_port = nil; ftpsvr_username = nil; ftpsvr_passwd = nil + if not option[:fdomain].nil? then + ftp_result = Utils.parse_ftpserver_url(option[:fdomain]) + if ftp_result.nil? or ftp_result.length != 4 then + puts "FTP server url is incorrect. (#{option[:fdomain]})" + puts "Tune as following format." + puts " ftp://:@
:" + exit 1 + end + ftpsvr_addr = ftp_result[0] + ftpsvr_port = ftp_result[1] + ftpsvr_username = ftp_result[2] + ftpsvr_passwd = ftp_result[3] + end + BuildServerController.create_server( option[:name], Utils::WORKING_DIR, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd ) when "remove" - BuildServerController.remove_server( option[:name] ) + BuildServerController.remove_server( option[:name] ) + when "migrate" + BuildServerController.migrate_server( option[:name], option[:db_dsn], option[:db_user], option[:db_passwd] ) when "start" - BuildServerController.start_server( option[:name], option[:port] ) - when "build" - if not option[:git].nil? then - if option[:resolve] then - BuildServerController.resolve_git( option[:name], option[:git], option[:commit], option[:os], nil ) - else - BuildServerController.build_git( option[:name], option[:git], option[:commit], option[:os], nil ) + if( option[:child] ) then # Child Process + BuildServerController.start_server( option[:name], option[:port] ) + else # Parent Process + # check server config + if not File.exist? "#{BuildServer::CONFIG_ROOT}/#{option[:name]}/server.cfg" + raise RuntimeError, "The server \"#{option[:name]}\" does not exist!" + end + + log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{option[:name]}/main.log" ) + begin + while(true) + log.info "Build Server[#{option[:name]}] Start - PORT:[#{option[:port]}]" + # Start child process + cmd = Utils.generate_shell_command("#{File.dirname(__FILE__)}/build-svr start -n #{option[:name]} -p #{option[:port]} --CHILD") + IO.popen(cmd) + pid = Process.wait + + # End chlid process + log.info "Child process terminated, pid = #{pid}, status = #{$?.exitstatus}" + if ($?.exitstatus == 0) then # SERVER STOP COMMAND + log.info "Down Build Server." + break + elsif ($?.exitstatus == 99) then # DIBS UPGRADE + cmd = "#{File.dirname(__FILE__)}/upgrade -l #{File.dirname(__FILE__)} -S -t BUILDSERVER -n #{option[:name]} -p #{option[:port]}" + if File.exist? "#{BuildServer::CONFIG_ROOT}/#{option[:name]}/upgrade_dist" then + File.open("#{BuildServer::CONFIG_ROOT}/#{option[:name]}/upgrade_dist","r") do |f| + f.each_line do |l| + cmd += " -D #{l.strip}" + break + end + end + File.delete "#{BuildServer::CONFIG_ROOT}/#{option[:name]}/upgrade_dist" + end + + cmd = Utils.generate_shell_command(cmd) + puts cmd + Utils.spawn(cmd) + log.info cmd + log.info "Down Build Server for DIBS upgrade." + break + else + log.error "Down Build Server. Try reboot Build Server." + end + end + rescue => e + log.error( e.message, Log::LV_USER) end - elsif not option[:local].nil? then - if option[:resolve] then - BuildServerController.resolve_local( option[:name], option[:local], option[:os], nil ) + end + when "stop" + BuildServerController.stop_server( option[:name] ) + + when "upgrade" + if not (option[:dist].nil? or option[:dist].empty?) then + build_server = BuildServerController.get_server(option[:name]) + # distribution check + if not build_server.distmgr.get_distribution(option[:dist]).nil? then + File.open("#{BuildServer::CONFIG_ROOT}/#{option[:name]}/upgrade_dist","w") do |f| + f.puts option[:dist] + end else - BuildServerController.build_local( option[:name], option[:local], option[:os], nil ) + puts "Distribution \"#{option[:dist]}\" is not exits" + puts "Upgrade Failed!!" + exit 1 end + end + + BuildServerController.upgrade_server( option[:name] ) + + when "add-svr" + svr_result = Utils.parse_server_addr(option[:domain]) + if svr_result.nil? or svr_result.length != 2 then + puts "Server address is incorrect. Tune as following format." + puts " :" + exit 1 + end + pkgsvr_addr = svr_result[0] + pkgsvr_port = svr_result[1] + BuildServerController.add_remote_server( option[:name], pkgsvr_addr, pkgsvr_port ) + + when "remove-svr" + BuildServerController.remove_remote_server( option[:name], pkgsvr_addr, pkgsvr_port ) + + when "add-os" + BuildServerController.add_target_os( option[:name], option[:os] ) + + when "remove-os" + BuildServerController.remove_target_os( option[:name], option[:os] ) + + when "add-dist" + svr_result = Utils.parse_server_addr(option[:domain]) + if svr_result.nil? or svr_result.length != 2 then + puts "Server address is incorrect. (#{option[:domain]})" + puts "Tune as following format." + puts " :" + exit 1 + end + pkgsvr_url = option[:url] + pkgsvr_addr = svr_result[0] + pkgsvr_port = svr_result[1] + BuildServerController.add_distribution( option[:name], option[:dist], pkgsvr_url, pkgsvr_addr, pkgsvr_port ) + + when "remove-dist" + BuildServerController.remove_distribution( option[:name], option[:dist] ) + + when "lock-dist" + BuildServerController.lock_distribution( option[:name], option[:dist] ) + + when "unlock-dist" + BuildServerController.unlock_distribution( option[:name], option[:dist] ) + + when "add-sync" + BuildServerController.add_sync_package_server( option[:name], option[:url], option[:dist] ) + + when "remove-sync" + BuildServerController.remove_sync_package_server( option[:name], option[:url], option[:dist] ) + + when "add-prj" + if not option[:git].nil? then + BuildServerController.add_project( option[:name], option[:pid], + option[:git], option[:branch], option[:remote], option[:passwd], + option[:os], option[:dist] ) else - RuntimeError "Wrong build options are specified!" + BuildServerController.add_binary_project( option[:name], option[:pid], + option[:package], option[:passwd], option[:os], option[:dist] ) end - when "add" - BuildServerController.add_friend_server( option[:name], option[:domain], option[:port] ) + + when "remove-prj" + BuildServerController.remove_project( option[:name], option[:pid], option[:dist] ) + + when "fullbuild" + BuildServerController.build_all_projects( option[:name], option[:dist] ) + + when "register" + BuildServerController.register_package( option[:name], option[:package], option[:dist] ) + + when "query" + BuildServerController.query_server( option[:name] ) + + when "set-attr" + BuildServerController.set_server_attribute( option[:name], option[:attr], option[:value] ) + + when "get-attr" + BuildServerController.get_server_attribute( option[:name], option[:attr] ) + else raise RuntimeError, "input option incorrect : #{option[:cmd]}" end diff --git a/dibs-web/Gemfile b/dibs-web/Gemfile new file mode 100644 index 0000000..cfe9b05 --- /dev/null +++ b/dibs-web/Gemfile @@ -0,0 +1,50 @@ +source 'https://rubygems.org' + +gem 'rails', '3.2.8' + +# Bundle edge Rails instead: +# gem 'rails', :git => 'git://github.com/rails/rails.git' + +gem 'sqlite3' + +gem 'json' + +gem 'execjs' + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', '~> 3.2.3' + gem 'coffee-rails', '~> 3.2.1' + + # See https://github.com/sstephenson/execjs#readme for more supported runtimes + # gem 'therubyracer', :platforms => :ruby + + gem 'uglifier', '>= 1.0.3' +end + +gem 'jquery-rails' + +# mysql +gem 'mysql2', '> 0.3' # as stated above + +# user +gem 'builder' + +# For encrypt password +gem "bcrypt-ruby", :require => "bcrypt" + +# To use ActiveModel has_secure_password +# gem 'bcrypt-ruby', '~> 3.0.0' + +# To use Jbuilder templates for JSON +# gem 'jbuilder' + +# Use unicorn as the app server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + +# To use debugger +# gem 'ruby-debug' diff --git a/dibs-web/Gemfile.lock b/dibs-web/Gemfile.lock new file mode 100644 index 0000000..d4f7f1a --- /dev/null +++ b/dibs-web/Gemfile.lock @@ -0,0 +1,118 @@ +GEM + remote: https://rubygems.org/ + specs: + actionmailer (3.2.8) + actionpack (= 3.2.8) + mail (~> 2.4.4) + actionpack (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + builder (~> 3.0.0) + erubis (~> 2.7.0) + journey (~> 1.0.4) + rack (~> 1.4.0) + rack-cache (~> 1.2) + rack-test (~> 0.6.1) + sprockets (~> 2.1.3) + activemodel (3.2.8) + activesupport (= 3.2.8) + builder (~> 3.0.0) + activerecord (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + arel (~> 3.0.2) + tzinfo (~> 0.3.29) + activeresource (3.2.8) + activemodel (= 3.2.8) + activesupport (= 3.2.8) + activesupport (3.2.8) + i18n (~> 0.6) + multi_json (~> 1.0) + arel (3.0.2) + bcrypt-ruby (3.0.1) + builder (3.0.3) + coffee-rails (3.2.2) + coffee-script (>= 2.2.0) + railties (~> 3.2.0) + coffee-script (2.2.0) + coffee-script-source + execjs + coffee-script-source (1.3.3) + erubis (2.7.0) + execjs (1.4.0) + multi_json (~> 1.0) + hike (1.2.1) + i18n (0.6.1) + journey (1.0.4) + jquery-rails (2.1.3) + railties (>= 3.1.0, < 5.0) + thor (~> 0.14) + json (1.7.5) + mail (2.4.4) + i18n (>= 0.4.0) + mime-types (~> 1.16) + treetop (~> 1.4.8) + mime-types (1.19) + multi_json (1.3.6) + mysql2 (0.3.11) + polyglot (0.3.3) + rack (1.4.1) + rack-cache (1.2) + rack (>= 0.4) + rack-ssl (1.3.2) + rack + rack-test (0.6.2) + rack (>= 1.0) + rails (3.2.8) + actionmailer (= 3.2.8) + actionpack (= 3.2.8) + activerecord (= 3.2.8) + activeresource (= 3.2.8) + activesupport (= 3.2.8) + bundler (~> 1.0) + railties (= 3.2.8) + railties (3.2.8) + actionpack (= 3.2.8) + activesupport (= 3.2.8) + rack-ssl (~> 1.3.2) + rake (>= 0.8.7) + rdoc (~> 3.4) + thor (>= 0.14.6, < 2.0) + rake (0.9.2.2) + rdoc (3.12) + json (~> 1.4) + sass (3.2.1) + sass-rails (3.2.5) + railties (~> 3.2.0) + sass (>= 3.1.10) + tilt (~> 1.3) + sprockets (2.1.3) + hike (~> 1.2) + rack (~> 1.0) + tilt (~> 1.1, != 1.3.0) + sqlite3 (1.3.6) + thor (0.16.0) + tilt (1.3.3) + treetop (1.4.10) + polyglot + polyglot (>= 0.3.1) + tzinfo (0.3.33) + uglifier (1.3.0) + execjs (>= 0.3.0) + multi_json (~> 1.0, >= 1.0.2) + +PLATFORMS + ruby + +DEPENDENCIES + bcrypt-ruby + builder + coffee-rails (~> 3.2.1) + execjs + jquery-rails + json + mysql2 (> 0.3) + rails (= 3.2.8) + sass-rails (~> 3.2.3) + sqlite3 + uglifier (>= 1.0.3) diff --git a/dibs-web/README.rdoc b/dibs-web/README.rdoc new file mode 100644 index 0000000..7c36f23 --- /dev/null +++ b/dibs-web/README.rdoc @@ -0,0 +1,261 @@ +== Welcome to Rails + +Rails is a web-application framework that includes everything needed to create +database-backed web applications according to the Model-View-Control pattern. + +This pattern splits the view (also called the presentation) into "dumb" +templates that are primarily responsible for inserting pre-built data in between +HTML tags. The model contains the "smart" domain objects (such as Account, +Product, Person, Post) that holds all the business logic and knows how to +persist themselves to a database. The controller handles the incoming requests +(such as Save New Account, Update Product, Show Post) by manipulating the model +and directing data to the view. + +In Rails, the model is handled by what's called an object-relational mapping +layer entitled Active Record. This layer allows you to present the data from +database rows as objects and embellish these data objects with business logic +methods. You can read more about Active Record in +link:files/vendor/rails/activerecord/README.html. + +The controller and view are handled by the Action Pack, which handles both +layers by its two parts: Action View and Action Controller. These two layers +are bundled in a single package due to their heavy interdependence. This is +unlike the relationship between the Active Record and Action Pack that is much +more separate. Each of these packages can be used independently outside of +Rails. You can read more about Action Pack in +link:files/vendor/rails/actionpack/README.html. + + +== Getting Started + +1. At the command prompt, create a new Rails application: + rails new myapp (where myapp is the application name) + +2. Change directory to myapp and start the web server: + cd myapp; rails server (run with --help for options) + +3. Go to http://localhost:3000/ and you'll see: + "Welcome aboard: You're riding Ruby on Rails!" + +4. Follow the guidelines to start developing your application. You can find +the following resources handy: + +* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html +* Ruby on Rails Tutorial Book: http://www.railstutorial.org/ + + +== Debugging Rails + +Sometimes your application goes wrong. Fortunately there are a lot of tools that +will help you debug it and get it back on the rails. + +First area to check is the application log files. Have "tail -f" commands +running on the server.log and development.log. Rails will automatically display +debugging and runtime information to these files. Debugging info will also be +shown in the browser on requests from 127.0.0.1. + +You can also log your own messages directly into the log file from your code +using the Ruby logger class from inside your controllers. Example: + + class WeblogController < ActionController::Base + def destroy + @weblog = Weblog.find(params[:id]) + @weblog.destroy + logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") + end + end + +The result will be a message in your log file along the lines of: + + Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! + +More information on how to use the logger is at http://www.ruby-doc.org/core/ + +Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are +several books available online as well: + +* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) +* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) + +These two books will bring you up to speed on the Ruby language and also on +programming in general. + + +== Debugger + +Debugger support is available through the debugger command when you start your +Mongrel or WEBrick server with --debugger. This means that you can break out of +execution at any point in the code, investigate and change the model, and then, +resume execution! You need to install ruby-debug to run the server in debugging +mode. With gems, use sudo gem install ruby-debug. Example: + + class WeblogController < ActionController::Base + def index + @posts = Post.all + debugger + end + end + +So the controller will accept the action, run the first line, then present you +with a IRB prompt in the server window. Here you can do things like: + + >> @posts.inspect + => "[#nil, "body"=>nil, "id"=>"1"}>, + #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" + >> @posts.first.title = "hello from a debugger" + => "hello from a debugger" + +...and even better, you can examine how your runtime objects actually work: + + >> f = @posts.first + => #nil, "body"=>nil, "id"=>"1"}> + >> f. + Display all 152 possibilities? (y or n) + +Finally, when you're ready to resume execution, you can enter "cont". + + +== Console + +The console is a Ruby shell, which allows you to interact with your +application's domain model. Here you'll have all parts of the application +configured, just like it is when the application is running. You can inspect +domain models, change values, and save to the database. Starting the script +without arguments will launch it in the development environment. + +To start the console, run rails console from the application +directory. + +Options: + +* Passing the -s, --sandbox argument will rollback any modifications + made to the database. +* Passing an environment name as an argument will load the corresponding + environment. Example: rails console production. + +To reload your controllers and models after launching the console run +reload! + +More information about irb can be found at: +link:http://www.rubycentral.org/pickaxe/irb.html + + +== dbconsole + +You can go to the command line of your database directly through rails +dbconsole. You would be connected to the database with the credentials +defined in database.yml. Starting the script without arguments will connect you +to the development database. Passing an argument will connect you to a different +database, like rails dbconsole production. Currently works for MySQL, +PostgreSQL and SQLite 3. + +== Description of Contents + +The default directory structure of a generated Ruby on Rails application: + + |-- app + | |-- assets + | |-- images + | |-- javascripts + | `-- stylesheets + | |-- controllers + | |-- helpers + | |-- mailers + | |-- models + | `-- views + | `-- layouts + |-- config + | |-- environments + | |-- initializers + | `-- locales + |-- db + |-- doc + |-- lib + | `-- tasks + |-- log + |-- public + |-- script + |-- test + | |-- fixtures + | |-- functional + | |-- integration + | |-- performance + | `-- unit + |-- tmp + | |-- cache + | |-- pids + | |-- sessions + | `-- sockets + `-- vendor + |-- assets + `-- stylesheets + `-- plugins + +app + Holds all the code that's specific to this particular application. + +app/assets + Contains subdirectories for images, stylesheets, and JavaScript files. + +app/controllers + Holds controllers that should be named like weblogs_controller.rb for + automated URL mapping. All controllers should descend from + ApplicationController which itself descends from ActionController::Base. + +app/models + Holds models that should be named like post.rb. Models descend from + ActiveRecord::Base by default. + +app/views + Holds the template files for the view that should be named like + weblogs/index.html.erb for the WeblogsController#index action. All views use + eRuby syntax by default. + +app/views/layouts + Holds the template files for layouts to be used with views. This models the + common header/footer method of wrapping views. In your views, define a layout + using the layout :default and create a file named default.html.erb. + Inside default.html.erb, call <% yield %> to render the view using this + layout. + +app/helpers + Holds view helpers that should be named like weblogs_helper.rb. These are + generated for you automatically when using generators for controllers. + Helpers can be used to wrap functionality for your views into methods. + +config + Configuration files for the Rails environment, the routing map, the database, + and other dependencies. + +db + Contains the database schema in schema.rb. db/migrate contains all the + sequence of Migrations for your schema. + +doc + This directory is where your application documentation will be stored when + generated using rake doc:app + +lib + Application specific libraries. Basically, any kind of custom code that + doesn't belong under controllers, models, or helpers. This directory is in + the load path. + +public + The directory available for the web server. Also contains the dispatchers and the + default HTML files. This should be set as the DOCUMENT_ROOT of your web + server. + +script + Helper scripts for automation and generation. + +test + Unit and functional tests along with fixtures. When using the rails generate + command, template test files will be generated for you and placed in this + directory. + +vendor + External libraries that the application depends on. Also includes the plugins + subdirectory. If the app has frozen rails, those gems also go here, under + vendor/rails/. This directory is in the load path. diff --git a/dibs-web/Rakefile b/dibs-web/Rakefile new file mode 100644 index 0000000..f818fd1 --- /dev/null +++ b/dibs-web/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +Dibs::Application.load_tasks diff --git a/dibs-web/app/assets/images/rails.png b/dibs-web/app/assets/images/rails.png new file mode 100644 index 0000000000000000000000000000000000000000..d5edc04e65f555e3ba4dcdaad39dc352e75b575e GIT binary patch literal 6646 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 AQ~&?~ literal 0 HcmV?d00001 diff --git a/dibs-web/app/controllers/admin_controller.rb b/dibs-web/app/controllers/admin_controller.rb new file mode 100644 index 0000000..b2803d5 --- /dev/null +++ b/dibs-web/app/controllers/admin_controller.rb @@ -0,0 +1,79 @@ +=begin + admin_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminController < ApplicationController + + before_filter :check_login_status, :check_admin_group + + def queryAllOS + os_list = SupportedOs.all(:order => "name") + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not os_list.nil? + os_list.each do |os| + doc.OS { + doc.OsName(os.name) + category = OsCategory.find(:first, :conditions => ["id = ?", os.os_category_id]) + if not category.nil? + doc.OsCategory(category.name) + end + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryAllOSCategory + os_category_list = OsCategory.all(:order => "name") + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not os_category_list.nil? + os_category_list.each do |category| + doc.OsCategoryName(category.name) + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + +end diff --git a/dibs-web/app/controllers/admin_distribution_controller.rb b/dibs-web/app/controllers/admin_distribution_controller.rb new file mode 100644 index 0000000..6d838c6 --- /dev/null +++ b/dibs-web/app/controllers/admin_distribution_controller.rb @@ -0,0 +1,261 @@ +=begin + admin_distribution_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminDistributionController < ApplicationController + + before_filter :check_login_status, :check_admin_group + + def queryAllDistribution + # get full distribution list + distributions = Distribution.all + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + #generate to XML + doc.Data { + if not distributions.nil? + distributions.each do |distribution| + doc.DistributionName(distribution.name) + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryDistributionInfo + dist_name = params[:distribution] + + if dist_name.nil? or dist_name.empty? + render :text => "Distribution name is empty", :content_type => "text/xml", :status => 406 + return + end + + # get distribution + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if distribution.nil? + render :text => "Can't find distribution : #{dist_name}", :content_type => "text/xml", :status => 406 + return + end + + # get sync package server + sync_package_server = SyncPkgServer.find(:first, :conditions => ["distribution_id = ?", distribution.id]) + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + #generate to XML + doc.Data{ + doc.DistributionInfo{ + doc.DistributionName(distribution.name) + doc.PackageServerUrl(distribution.pkgsvr_url) + doc.PackageServerAddress(distribution.pkgsvr_addr) + doc.Status(distribution.status) + doc.Description(distribution.description) + } + + doc.SyncPackageServer{ + if sync_package_server.nil? + doc.Url("") + doc.Period("") + doc.Description("") + else + doc.Url(sync_package_server.pkgsvr_url) + doc.Period(sync_package_server.period) + doc.Description(sync_package_server.description) + end + } + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def addDistribution + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + dist_name = change_item[:DistributionName] + if dist_name.nil? or dist_name.empty? then + errmsg = "Can't find [#{dist_name}] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + dist = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if dist.nil? + dist = Distribution.new + dist.name = change_item[:DistributionName] + dist.pkgsvr_url = change_item[:URL] + dist.pkgsvr_addr = change_item[:Address] + dist.status = change_item[:DistStatus] + dist.description = change_item[:Description] + dist.save + + sync_pkg_svr = SyncPkgServer.new + sync_pkg_svr.distribution_id = dist.id + sync_pkg_svr.pkgsvr_url = change_item[:SyncPkgSvrUrl] + sync_pkg_svr.period = change_item[:SyncPkgSvrPeriod] + sync_pkg_svr.description = change_item[:SyncPkgSvrDescription] + sync_pkg_svr.save + else + errmsg = "Distribution[#{dist_name}] already exist" + end + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :json => { :error=> errmsg }, :status => 406 + end + end + + def removeDistribution + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + dist_name = change_item[:DistributionName] + if dist_name.nil? or dist_name.empty? then + errmsg = "Can't find [#{dist_name}] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + dist = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if dist.nil? + errmsg = "Distribution[#{dist_name}] not exist" + else + # first, remove sync package server + sync_pkg_svr = SyncPkgServer.find(:first, :conditions => ["distribution_id = ?", dist.id]) + + if not sync_pkg_svr.nil? + sync_pkg_svr.destroy + end + + # remove distribution + dist.destroy + end + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :json => { :error=> errmsg }, :status => 406 + end + end + + def modifyDistribution + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + dist_name = change_item[:DistributionName] + if dist_name.nil? or dist_name.empty? then + errmsg = "Can't find [#{dist_name}] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + dist = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if dist.nil? + errmsg = "Distribution[#{dist_name}] not exist" + else + dist.pkgsvr_url = change_item[:URL] + dist.pkgsvr_addr = change_item[:Address] + dist.status = change_item[:DistStatus] + dist.description = change_item[:Description] + dist.save + + sync_pkg_svr_url = change_item[:SyncPkgSvrUrl] + sync_pkg_svr_period = change_item[:SyncPkgSvrPeriod] + sync_pkg_svr_description = change_item[:SyncPkgSvrDescription] + sync_pkg_svr = SyncPkgServer.find(:first, :conditions => ["distribution_id = ?", dist.id]) + if not sync_pkg_svr_url.nil? and not sync_pkg_svr_url.empty? + if sync_pkg_svr.nil? + sync_pkg_svr = SyncPkgServer.new + end + sync_pkg_svr.distribution_id = dist.id + sync_pkg_svr.pkgsvr_url = sync_pkg_svr_url + sync_pkg_svr.period = sync_pkg_svr_period + sync_pkg_svr.description = sync_pkg_svr_description + sync_pkg_svr.save + elsif not sync_pkg_svr.nil? and not sync_pkg_svr_url.nil? and sync_pkg_svr_url.empty? + # if sync_pkg_svr is already exist and modified sync_pkg_svr_url is empty then remove it + sync_pkg_svr.destroy + end + end + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :json => { :error=> errmsg }, :status => 406 + end + end + + def fullBuildDistribution + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + dist_name = change_item[:DistributionName] + if dist_name.nil? or dist_name.empty? then + errmsg = "Can't find [#{dist_name}] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + dist = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if dist.nil? + errmsg = "Distribution[#{dist_name}] not exist" + end + + server_config = Server_config.find(:first, :conditions => ["property = \"id\""]) + if server_config.nil? + errmsg = "Server name can't find" + else + server_name = server_config.value + + if server_name.nil? or server_name.empty? + errmsg = "Server name can't find" + end + end + + if errmsg.empty? + Utils.sbi_fullbuild_command(server_name, dist_name) + render :json => { :success => "OK!" } + else + render :json => { :error=> errmsg }, :status => 406 + end + end +end diff --git a/dibs-web/app/controllers/admin_group_controller.rb b/dibs-web/app/controllers/admin_group_controller.rb new file mode 100644 index 0000000..5818e1b --- /dev/null +++ b/dibs-web/app/controllers/admin_group_controller.rb @@ -0,0 +1,240 @@ +=begin + admin_group_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminGroupController < ApplicationController + + before_filter :check_login_status, :check_admin_group + skip_before_filter :check_admin_group, :only => [:queryAllGroup] + + def queryAllGroup + project_list = Project.all(:order => "name") + group_list = Group.all + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not group_list.nil? + group_list.each do |group| + group_access_list = GroupProjectAccess.find(:all, :conditions => ["group_id = ?", group.id]) + + doc.Group { + doc.GroupName(group.name) + doc.AdminFlag(group.admin) + doc.Description(group.description) + + if not group_access_list.nil? + group_access_list.each do |group_right| + project = Project.find(:first, :conditions => ["id = ?", group_right.project_id]); + distribution = Distribution.find(:first, :conditions => ["id = ?", project.distribution_id]); + if not project.nil? + doc.AccessableProject { + doc.ProjectName(project.name) + doc.ProjectDistribution(distribution.name) + } + end + end + end + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryGroupInfo + group_name = params[:groupName] + if group_name.nil? or group_name.empty? + render :text => "Group name is empty", :content_type => "text/xml", :status => 406 + return + end + + group = Group.find(:first, :conditions => ["name = ?", group_name]) + if group.nil? + render :text => "Can't find group : #{group_name}", :content_type => "text/xml", :status => 406 + return + end + + project_list = Project.all(:order => "name") + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + if not project_list.nil? + project_list.each do |project| + distribution = Distribution.find(:first, :conditions => ["id = ?", project.distribution_id]); + doc.Project{ + doc.Name(project.name) + doc.Id(project.id) + doc.DistName(distribution.name) + } + end + end + + doc.Group { + doc.Name(group.name) + + group_access_list = GroupProjectAccess.find(:all, :conditions => ["group_id = ?", group.id]) + + if not group_access_list.nil? and not group_access_list.empty? + project_id_list = [] + group_access_list.each do |group_right| + project_id_list.push group_right.project_id + end + + doc.ProjectList(project_id_list.join(",")) + end + } + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def modifyGroup + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + group_name = change_item[:GroupName] + if group_name.nil? or group_name.empty? + render :text => "Group name is invalid", :status => 406 + return + else + group = Group.find(:first, :conditions => ["name= ?", group_name]) + end + + if group.nil? + errmsg = "Can't find group" + else + new_group_name = change_item[:NewGroupName] + admin_flag = change_item[:AdminFlag] + description = change_item[:Description] + project_id_list = change_item[:ProjectList].split(",") + + group.name = new_group_name + group.admin = admin_flag + group.description = description + group.save + + GroupProjectAccess.delete_all(["group_id = ?", group.id]) + + project_id_list.each do |project_id| + item = GroupProjectAccess.new + item.group_id = group.id + item.project_id = project_id + item.build = "TRUE" + item.save + end + end + + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :text => errmsg, :status => 406 + end + end + + def removeGroup + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + group_name = change_item[:GroupName] + if group_name.nil? or group_name.empty? + render :text => "Group name is invalid", :status => 406 + return + else + group = Group.find(:first, :conditions => ["name= ?", group_name]) + end + + if group.nil? + errmsg = "Can't find group" + else + user_group = UserGroup.find(:first, :conditions => ["group_id = ?", group.id]) + if user_group.nil? + GroupProjectAccess.delete_all(["group_id = ?", group.id]) + group.destroy + else + errmsg = "Can't remove. #{group_name} has users." + end + end + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :text => errmsg, :status => 406 + end + end + + def addGroup + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + group_name = change_item[:GroupName] + if group_name.nil? or group_name.empty? + render :text => "Group name is invalid", :status => 406 + return + else + group = Group.find(:first, :conditions => ["name= ?", group_name]) + end + + if not group.nil? + errmsg = "Group already exist" + else + group = Group.new + group.name = group_name + group.admin = change_item[:AdminFlag] + group.description = change_item[:Description] + group.save + + project_id_list = change_item[:ProjectList].split(",") + project_id_list.each do |project_id| + group_project_access = GroupProjectAccess.new + group_project_access.group_id = group.id + group_project_access.project_id = project_id + group_project_access.save + end + end + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :text => errmsg, :status => 406 + end + end +end diff --git a/dibs-web/app/controllers/admin_project_controller.rb b/dibs-web/app/controllers/admin_project_controller.rb new file mode 100644 index 0000000..a0f7b72 --- /dev/null +++ b/dibs-web/app/controllers/admin_project_controller.rb @@ -0,0 +1,301 @@ +=begin + admin_project_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminProjectController < ApplicationController + def queryAllProject + project_list = Project.all(:order => "name") + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not project_list.nil? + project_list.each do |project| + distribution = Distribution.find(:first, :conditions => ["id = ?", project.distribution_id]); + doc.Project{ + doc.Name(project.name) + doc.Id(project.id) + doc.DistName(distribution.name) + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryProjectsInDistributionForAdmin + dist_name = params[:distribution] + + if dist_name.nil? or dist_name.empty? + render :text => "Distribution name is empty", :content_type => "text/xml", :status => 406 + return + end + + # get distribution + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + if distribution.nil? + render :text => "Can't find distribution : #{dist_name}", :content_type => "text/xml", :status => 406 + return + end + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + #generate to XML + doc.Data{ + project_list = Project.find_by_sql("SELECT projects.id + , projects.name + , projects.ptype + FROM projects + , distributions + WHERE distributions.name = \"#{dist_name}\" + AND distributions.id = projects.distribution_id + ORDER BY projects.name") + if not project_list.nil? + project_list.each do |project| + doc.Project { + doc.ProjectName(project.name) + doc.Type(project.ptype) + + os_list = ProjectOs.find(:all, :conditions => ["project_id = ?", project.id]) + if not os_list.nil? then + os_list.each do |os| + supported_os = SupportedOs.find(:first, :conditions => ["id = ?", os.supported_os_id]) + doc.OS(supported_os.name) + end + end + + case project.ptype.upcase + when "GIT" + git = ProjectGit.find(:first, :conditions => ["project_id = ?", project.id]) + if not git.nil? then + doc.GitRepos(git.git_repos) + doc.GitBranch(git.git_branch) + end + + when "BINARY" + bin = ProjectBin.find(:first, :conditions => ["project_id = ?", project.id]) + if not bin.nil? then + doc.PackageName(bin.pkg_name) + end + end + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def addProject + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + project_name = change_item[:Name] + project_type = change_item[:ProjectType].upcase + project_dist_name = change_item[:Distribution] + project_password = change_item[:ProjectPass] + + if project_name.nil? or project_name.empty? then + errmsg = "Can't find [Name] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + distribution = Distribution.find(:first, :conditions => ["name = ?", project_dist_name]) + project = Project.find(:first, :conditions => ["name = ? and distribution_id = ? and ptype = ?", project_name, distribution.id, project_type]) + + if not project.nil? + errmsg = "project already exist" + render :json => { :error => errmsg }, :status => 406 + return + end + + if not project_type.eql? "GIT" and not project_type.eql? "BINARY" + errmsg = "project type is invalid" + render :json => { :error => errmsg }, :status => 406 + return + end + + project = Project.new + project.name = project_name + project.ptype = project_type + project.password = project_password + project.distribution_id = distribution.id + #TODO: set project user is admin. admin user id is '1' + project.user_id = 1 + project.save + + if not change_item[:OSNameList].nil? + os_name_list = change_item[:OSNameList].split(",") + os_name_list.each do |os_name| + supported_os = SupportedOs.find(:first, :conditions => ["name = ?", os_name]) + #need check not found + project_os = ProjectOs.new + project_os.project_id = project.id + project_os.supported_os_id = supported_os.id + project_os.save + end + end + + case project.ptype + when "GIT" + git = ProjectGit.new + git.project_id = project.id + git.git_repos = change_item[:Address] + git.git_branch = change_item[:Branch] + git.save + when "BINARY" + binary = ProjectBin.new + binary.project_id = project.id + binary.pkg_name = change_item[:PackageName] + binary.save + end + + render :json => { :success => "OK!" } + end + + def removeProject + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + if change_item[:Name].nil? or change_item[:Name].empty? then + errmsg = "Can't find [Name] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + dist_name = change_item[:Distribution] + project_name = change_item[:Name] + project_type = change_item[:ProjectType] + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + project = Project.find(:first, :conditions => ["name = ? and distribution_id = ?", project_name, distribution.id]) + + if project.nil? + errmsg = "project does not exist" + render :json => { :error => errmsg }, :status => 406 + return + end + + case project_type.upcase + when "GIT" + ProjectGit.delete_all(["project_id = ?", project.id]) + ProjectOs.delete_all(["project_id = ?", project.id]) + GroupProjectAccess.delete_all(["project_id = ?", project.id]) + when "BINARY" + ProjectBin.delete_all(["project_id = ?", project.id]) + GroupProjectAccess.delete_all(["project_id = ?", project.id]) + else + errmsg = "project type is invalid" + render :json => { :error => errmsg }, :status => 406 + return + end + + # remove project os + ProjectOs.delete_all(["project_id = ?", project.id]) + + # remove project + project.destroy + + render :json => { :success => "OK!" } + end + + def modifyProject + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + dist_name = change_item[:Distribution] + old_project_name = change_item[:Name] + project_type = change_item[:ProjectType].upcase + + if old_project_name.nil? or old_project_name.empty? then + errmsg = "Can't find [#{old_project_name}] information" + render :json => { :error => errmsg }, :status => 406 + return + end + + if not project_type.eql? "GIT" and not project_type.eql? "BINARY" + errmsg = "project type is invalid" + render :json => { :error => errmsg }, :status => 406 + return + end + + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + project = Project.find(:first, :conditions => ["name = ? and distribution_id = ? and ptype = ?", old_project_name, distribution.id, project_type]) + project.name = change_item[:NewProjectName] + project.password = change_item[:ProjectPass] + + # remove project os and reset project os + ProjectOs.delete_all(["project_id = ?", project.id]) + if not change_item[:OSNameList].nil? + os_name_list = change_item[:OSNameList].split(",") + os_name_list.each do |os_name| + supported_os = SupportedOs.find(:first, :conditions => ["name = ?", os_name]) + #need check not found + project_os = ProjectOs.new + project_os.project_id = project.id + project_os.supported_os_id = supported_os.id + project_os.save + end + end + + case project_type + when "GIT" + project_git = ProjectGit.find(:first, :conditions => ["project_id = ?", project.id]) + if project_git.nil? + project_git.project_id = project.id + project_git = ProjectGit.new + end + project_git.git_repos = change_item[:ProjectAddress] + project_git.git_branch = change_item[:ProjectBranch] + project_git.save + + when "BINARY" + project_bin = ProjectBin.find(:first, :conditions => ["project_id = ?", project.id]) + if project_bin.nil? + project_bin = ProjectBin.new + project_bin.project_id = project.id + end + project_bin.pkg_name = change_item[:PackageName] + project_bin.save + end + + project.save + render :json => { :success => "OK!" } + end +end diff --git a/dibs-web/app/controllers/admin_server_controller.rb b/dibs-web/app/controllers/admin_server_controller.rb new file mode 100644 index 0000000..8efb20d --- /dev/null +++ b/dibs-web/app/controllers/admin_server_controller.rb @@ -0,0 +1,424 @@ +=begin + admin_server_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminServerController < ApplicationController + def queryAllServer + # get full distribution list + server_config = Server_config.all + remote_build_servers = RemoteBuildServer.all + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + #generate to XML + doc.Data { + if not server_config.nil? + server_config.each do |info| + doc.ServerConfig { + doc.Property( info.property ) + doc.Value( info.value ) + } + end + end + + if not remote_build_servers.nil? + remote_build_servers.each do |server| + doc.RemoteBuildServer { + doc.Address(server.svr_addr) + supported_os = SupportedOs.find(:first, :conditions => ["id = ?", server.supported_os_id]) + if supported_os.nil? + doc.SupportedOS("") + else + doc.SupportedOS(supported_os.name) + end + doc.Status(server.status) + doc.MaxJobCount(server.max_job_count) + doc.WorkingJobCount(server.working_job_count) + doc.WaitingJobCount(server.waiting_job_count) + doc.Description(server.description) + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def addRemoteBuildServer + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + address = change_item[:Address] + description = change_item[:Description] + if address.nil? or address.empty? + errmsg = "Server address is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + server = RemoteBuildServer.find(:first, :conditions => ["svr_addr = ?", address]) + + if server.nil? + server = RemoteBuildServer.new + server.svr_addr = address + server.description = description + server.save + + render :json => { :success => "OK!" } + else + errmsg = "Server already exist" + + render :json => { :error => errmsg }, :status => 406 + end + end + + def modifyRemoteBuildServer + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + address = change_item[:Address] + if address.nil? or address.empty? + errmsg = "Server address is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + server = RemoteBuildServer.find(:first, :conditions => ["svr_addr = ?", address]) + + if server.nil? + errmsg = "Server does not exist" + else + new_address = change_item[:NewAddress] + description = change_item[:Description] + + # error check for server already exist + if not address.eql? new_address + new_server = RemoteBuildServer.find(:first, :conditions => ["svr_addr = ?", new_address]) + + if not new_server.nil? + errmsg = "Server already exist" + end + end + + server.svr_addr = new_address + server.description = description + server.save + end + + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :json => { :error => errmsg }, :status => 406 + end + end + + def removeRemoteBuildServer + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + address = change_item[:Address] + if address.nil? or address.empty? + errmsg = "Server address is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + server = RemoteBuildServer.find(:first, :conditions => ["svr_addr = ?", address]) + + if server.nil? + errmsg = "Server does not exist" + render :json => { :error => errmsg }, :status => 406 + else + server.destroy + render :json => { :success => "OK!" } + end + end + + def addOsCategory + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + name = change_item[:Name] + if name.nil? or name.empty? + errmsg = "Os category name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + osCategory = OsCategory.find(:first, :conditions => ["name = ?", name]) + + if osCategory.nil? + osCategory= OsCategory.new + osCategory.name = name + osCategory.save + + render :json => { :success => "OK!" } + else + errmsg = "Os category already exist" + + render :json => { :error => errmsg }, :status => 406 + end + end + + def removeOsCategory + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + name = change_item[:Name] + if name.nil? or name.empty? + errmsg = "Os category name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + osCategory = OsCategory.find(:first, :conditions => ["name = ?", name]) + + if osCategory.nil? + errmsg = "Can't find os category" + render :json => { :error => errmsg }, :status => 406 + else + osCategory.destroy + render :json => { :success => "OK!" } + end + end + + def addSupportedOS + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + os_name = change_item[:Name] + category_name = change_item[:OsCategory] + if os_name.nil? or os_name.empty? + errmsg = "Os name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + if category_name.nil? or category_name.empty? + errmsg = "Os category is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + os_category = OsCategory.find(:first, :conditions => ["name = ?", category_name]) + supported_os = SupportedOs.find(:first, :conditions => ["name = ?", os_name]) + + if os_category.nil? + errmsg = "Os category does not exist" + + render :json => { :error => errmsg }, :status => 406 + elsif not supported_os.nil? + errmsg = "supported os already exist" + + render :json => { :error => errmsg }, :status => 406 + else + supported_os = SupportedOs.new + supported_os.name = os_name + supported_os.os_category_id = os_category.id + supported_os.save + + render :json => { :success => "OK!" } + end + end + + def removeSupportedOS + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + os_name = change_item[:Name] + if os_name.nil? or os_name.empty? + errmsg = "Os name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + supported_os = SupportedOs.find(:first, :conditions => ["name = ?", os_name]) + + if supported_os.nil? + errmsg = "supported os does not exist" + + render :json => { :error => errmsg }, :status => 406 + else + supported_os.destroy + + render :json => { :success => "OK!" } + end + end + + def modifySupportedOS + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + os_name = change_item[:Name] + new_os_name = change_item[:NewName] + category_name = change_item[:OsCategory] + if os_name.nil? or os_name.empty? + errmsg = "Os name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + os_category = OsCategory.find(:first, :conditions => ["name = ?", category_name]) + supported_os = SupportedOs.find(:first, :conditions => ["name = ?", os_name]) + + if os_category.nil? + errmsg = "Os category does not exist" + + render :json => { :error => errmsg }, :status => 406 + elsif supported_os.nil? + errmsg = "supported os does not exist" + + render :json => { :error => errmsg }, :status => 406 + else + if not os_name.eql? new_os_name + new_supported_os = SupportedOs.find(:first, :conditions => ["name = ?", new_os_name]) + if new_supported_os.nil? + supported_os.name = new_os_name + else + errmsg = "supported os already exist" + + render :json => { :error => errmsg }, :status => 406 + return + end + end + + supported_os.os_category_id = os_category.id + supported_os.save + + render :json => { :success => "OK!" } + end + end + + def addServerInfo + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + property = change_item[:Property] + value = change_item[:Value] + if property.nil? or property.empty? + errmsg = "Property name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + if value.nil? or value.empty? + errmsg = "Value is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + config = Server_config.find(:first, :conditions => ["property = ?", property]) + + if not config.nil? + errmsg = "Config alerady exist" + render :json => { :error => errmsg }, :status => 406 + else + config = Server_config.new + config.property = property + config.value = value + config.save + + render :json => { :success => "OK!" } + end + end + + def modifyServerInfo + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + property = change_item[:Property] + value = change_item[:Value] + if property.nil? or property.empty? + errmsg = "Property is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + if value.nil? or value.empty? + errmsg = "Value is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + config = Server_config.find(:first, :conditions => ["property = ?", property]) + + if config.nil? + errmsg = "Config does not exist" + render :json => { :error => errmsg }, :status => 406 + else + config.value = value + config.save + + render :json => { :success => "OK!" } + end + end + + def removeServerInfo + change_group_list = params[:ChangeInfoList] + change_item = change_group_list[0] + errmsg = "" + + property = change_item[:Property] + if property.nil? or property.empty? + errmsg = "Property is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + # get server for error check + config = Server_config.find(:first, :conditions => ["property = ?", property]) + + if config.nil? + errmsg = "Property does not exist" + render :json => { :error => errmsg }, :status => 406 + else + config.destroy + render :json => { :success => "OK!" } + end + end + +end diff --git a/dibs-web/app/controllers/admin_user_controller.rb b/dibs-web/app/controllers/admin_user_controller.rb new file mode 100644 index 0000000..7f4af7e --- /dev/null +++ b/dibs-web/app/controllers/admin_user_controller.rb @@ -0,0 +1,142 @@ +=begin + admin_user_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class AdminUserController < ApplicationController + + before_filter :check_login_status, :check_admin_group + + def queryAllUser + user_list = User.all(:order => "name") + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not user_list.nil? + user_list.each do |user| + doc.User { + doc.Name(user.name) + doc.Email(user.email) + doc.GroupList { + group_list = Group.find_by_sql("SELECT groups.name + , groups.admin + , groups.description + FROM users + , user_groups + , groups + WHERE users.id = user_groups.user_id + AND user_groups.group_id = groups.id + AND users.email = \"#{user.email}\"") + group_list.each { |group| + doc.GroupName(group.name) + } + } + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def modifyUser + change_user_list = params[:ChangeInfoList] + change_item = change_user_list[0] + errmsg = "" + + email = change_item[:Email] + group_name = change_item[:GroupName] + user_name = change_item[:UserName] + + if email.nil? or email.empty? + render :text => "Email is invalid", :status => 406 + return + else + user = User.find(:first, :conditions => ["email = ?", email]) + + if user.nil? + errmsg = "Can't find user information" + render :text => errmsg, :status => 406 + return + end + end + + if group_name.nil? or group_name.empty? + render :text => "Group name is invalid", :status => 406 + return + else + group = Group.find(:first, :conditions => ["name = ?", group_name]) + if group.nil? + errmsg = "Can't find group information" + render :text => errmsg, :status => 406 + return + end + end + + user.name = user_name + user.save + + UserGroup.delete_all(["user_id = ?", user.id]) + + user_groups = UserGroup.new + + user_groups.user_id = user.id + user_groups.group_id = group.id + user_groups.status = "ACTIVE" + user_groups.save + + render :json => { :success => "OK!" } + end + + def removeUser + change_user_list = params[:ChangeInfoList] + change_item = change_user_list[0] + errmsg = "" + + email = change_item[:Email] + if email.nil? or email.empty? + render :text => "Email is invalid", :status => 406 + return + else + user = User.find(:first, :conditions => ["email = ?", email]) + end + + if user.nil? + errmsg = "Can't find user information" + render :text => errmsg, :status => 406 + else + UserGroup.delete_all(["user_id = ?", user.id]) + + user.destroy + render :json => { :success => "OK!" } + end + end +end diff --git a/dibs-web/app/controllers/application_controller.rb b/dibs-web/app/controllers/application_controller.rb new file mode 100644 index 0000000..1ad4921 --- /dev/null +++ b/dibs-web/app/controllers/application_controller.rb @@ -0,0 +1,162 @@ +=begin + application_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class ApplicationController < ActionController::Base + #protect_from_forgery + + JOBS_STAUTS_WAITING = "WAITING" + + helper_method :current_user + + def get_user_id + if @current_user.nil? + current_user + end + + if not @current_user.nil? + return @current_user.id + else + return nil + end + end + + def get_user_email + if @current_user.nil? + current_user + end + + if not @current_user.nil? + return @current_user.email + else + return nil + end + end + + def get_group_list(email) + if not email.nil? + group_list = Group.find_by_sql("SELECT groups.id + , groups.name + , groups.admin + , groups.description + FROM users + , user_groups + , groups + WHERE users.id = user_groups.user_id + AND user_groups.group_id = groups.id + AND users.email = \"#{email}\"") + return group_list + else + return nil + end + end + + def is_admin(email) + if not email.nil? + group = Group.find_by_sql("SELECT count(*) AS cnt + FROM users + , user_groups + , groups + WHERE users.id = user_groups.user_id + AND user_groups.group_id = groups.id + AND groups.admin = 'TRUE' + AND users.email = \"#{email}\"") + if group[0].cnt > 0 + return TRUE + end + end + + return FALSE + end + + def generate_xml_header(doc) + email = get_user_email + group_list = get_group_list(email) + if is_admin(email) + admin = "TRUE" + else + admin = "FALSE" + end + + doc.Header { + doc.UserInfo { + doc.Email(email) + doc.Name(@current_user.name) + doc.Admin(admin) + doc.GroupList { + group_list.each { |group| + doc.Group { + doc.GroupName(group.name) + doc.GroupAdmin(group.admin) + doc.GroupDescription(group.description) + } + } + } + } + } + end + + private + def current_user + @current_user ||= User.find(:first, :conditions => {:email => session[:user_email]}) if session[:user_email] + end + + def check_login_status + if session[:user_email].nil? then + render :nothing => true, :status => 401 + return + end + end + + def check_admin_group + if session[:user_email].nil? or session[:user_email].empty? then + render :nothing => true, :status => 401 + return false + else + admin = "FALSE" + user_email = session[:user_email] + + user = User.find(:first, :conditions => ["email = ?", user_email]) + + group_list = get_group_list(user.email) + + group_list.each {|group| + if group.admin == "TRUE" + admin = "TRUE" + break; + end + } + + if user.nil? or admin != "TRUE" + render :nothing => true, :status => 401 + return false + end + end + + return true + end +end diff --git a/dibs-web/app/controllers/jobs_controller.rb b/dibs-web/app/controllers/jobs_controller.rb new file mode 100644 index 0000000..b4f3797 --- /dev/null +++ b/dibs-web/app/controllers/jobs_controller.rb @@ -0,0 +1,634 @@ +=begin + jobs_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class JobsController < ApplicationController + + before_filter :check_login_status + + QUERY_CNT = 30 + + def list + end + + def listAll + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + listQuery(doc, "QUERY", params[:distribution], params[:status], params[:lastID], "", "") + } + } + render :text => out_string, :content_type => "text/xml" + end + + def listSearchUser + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + listQuery(doc, "QUERY", params[:distribution], params[:status], params[:lastID], params[:user], "") + } + } + render :text => out_string, :content_type => "text/xml" + end + + def listSearchGroup + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + listQueryGroup(doc, "QUERY", params[:distribution], params[:status], params[:lastID], params[:group]) + } + } + render :text => out_string, :content_type => "text/xml" + end + + def listSearchProject + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + listQueryProject(doc, "QUERY", params[:distribution], params[:status], params[:lastID], params[:project]) + } + } + render :text => out_string, :content_type => "text/xml" + end + + def listSearchDate + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + listQuery(doc, "QUERY", params[:distribution], params[:status], params[:lastID], "", params[:date]) + } + } + render :text => out_string, :content_type => "text/xml" + end + + def updateList + condition = params[:Condition] + param = params[:Param] + distribution = params[:Distribution] + status = params[:Status] + latest_id = params[:LatestId] + working_job_list = params[:WorkingJobId] + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + #new job list + case condition + when "all" + listQuery(doc, "UPDATE", distribution, status, latest_id, "", "") + when "user" + listQuery(doc, "UPDATE", distribution, status, latest_id, param, "") + when "group" + listQueryGroup(doc, "UPDATE", distribution, status, latest_id, param) + when "project" + listQueryProject(doc, "UPDATE", distribution, status, latest_id, param) + when "date" + listQuery(doc, "UPDATE", distribution, status, latest_id, "", param) + else + end + + # working job list for update + doc.WorkingJobList { + if not working_job_list.nil? + working_job_list.each do |job_id| + job = get_job_info(job_id) + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute(job.job_attribute) + doc.ParentJobId(job.parent_job_id) + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + + if job.job_attribute == "MULTI" + child_jobs = get_child_job_info(job.job_id) + child_jobs.each {|job| + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute("CHILD") + doc.ParentJobId(job.parent_job_id) + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + } + end + end + end + } + } + } + + render :text => out_string, :content_type => "text/xml" + + end + + def listQuery(doc, query_type, distribution, status, last_id, user, date) + condition = "" + + if(distribution == "ALL") + distribution = "%" + end + + if(status == "ALL") + status = "'FINISHED', 'JUST_CREATED', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING', 'ERROR', 'CANCELED'" + elsif(status == "SUCCESS") + status = "'FINISHED'" + elsif(status == "WORKING") + status = "'JUST_CREATE', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING'" + elsif(status == "ERROR") + status = "'ERROR', 'CANCELED'" + end + + if(query_type == "QUERY") + if(last_id == "LATEST") + last_job_id = Job.maximum(:id) + if last_job_id.nil? + last_job_id = 0 + end + else + last_job_id = last_id.to_i - 1 + end + condition = "jobs.id <= #{last_job_id}" + else + condition = "jobs.id > #{last_id}" + end + + user = user + "%" + + date = date + "%" + + jobs = Job.find_by_sql("SELECT jobs.id + , CASE WHEN jobs.jtype like 'MULTI%' THEN 'MULTI' + ELSE 'SINGLE' END AS job_attribute + FROM jobs + LEFT JOIN projects + ON jobs.project_id = projects.id + LEFT JOIN users + ON jobs.user_id = users.id + LEFT JOIN supported_os + ON jobs.supported_os_id = supported_os.id + LEFT JOIN distributions + ON jobs.distribution_id = distributions.id + LEFT JOIN remote_build_servers + ON jobs.remote_build_server_id = remote_build_servers.id + LEFT JOIN sources + ON jobs.source_id = sources.id + WHERE #{condition} + AND jobs.parent_job_id IS NULL + AND jobs.status in (#{status}) + AND users.name like '#{user}' + AND distributions.name like '#{distribution}' + AND DATE(jobs.start_time) like '#{date}' + ORDER BY jobs.id DESC + LIMIT #{QUERY_CNT}") + + #generate to XML + doc.JobList { + jobs.each {|job_list| + job = get_job_info(job_list.id) + + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute(job_list.job_attribute) + doc.ParentJobId(job.parent_job_id) + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + + if job_list.job_attribute == "MULTI" + child_jobs = get_child_job_info(job.job_id) + child_jobs.each {|job| + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute("CHILD") + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + } + end + } + } + end + + def listQueryGroup(doc, query_type, distribution, status, last_id, group) + + if(distribution == "ALL") + distribution = "%" + end + + if(status == "ALL") + status = "'FINISHED', 'JUST_CREATED', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING', 'ERROR', 'CANCELED'" + elsif(status == "SUCCESS") + status = "'FINISHED'" + elsif(status == "WORKING") + status = "'JUST_CREATE', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING'" + elsif(status == "ERROR") + status = "'ERROR', 'CANCELED'" + end + + if(query_type == "QUERY") + if(last_id == "LATEST") + last_job_id = Job.maximum(:id) + if last_job_id.nil? + last_job_id = 0 + end + else + last_job_id = last_id.to_i - 1 + end + condition = "jobs.id <= #{last_job_id}" + else + condition = "jobs.id > #{last_id}" + end + + group = group + "%" + + jobs = Job.find_by_sql("SELECT jobs.id + , CASE WHEN jobs.jtype like 'MULTI%' THEN 'MULTI' + ELSE 'SINGLE' END AS job_attribute + FROM jobs + LEFT JOIN distributions ON jobs.distribution_id = distributions.id + WHERE #{condition} + AND jobs.parent_job_id IS NULL + AND distributions.name like '#{distribution}' + AND jobs.status in (#{status}) + AND jobs.user_id IN (SELECT users.id + FROM users + LEFT JOIN user_groups ON user_groups.user_id = users.id + LEFT JOIN groups ON groups.id = user_groups.group_id + WHERE groups.name LIKE '#{group}') + ORDER BY jobs.id DESC + LIMIT #{QUERY_CNT}") + + #generate to XML + doc.Data { + doc.JobList { + jobs.each {|job_list| + job = get_job_info(job_list.id) + + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute(job_list.job_attribute) + doc.ParentJobId(job.parent_job_id) + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + + if job_list.job_attribute == "MULTI" + child_jobs = get_child_job_info(job.job_id) + child_jobs.each {|job| + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute("CHILD") + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + } + end + } + } + } + end + + def listQueryProject(doc, query_type, distribution, status, last_id, project) + if(distribution == "ALL") + distribution = "%" + end + + if(status == "ALL") + status = "'FINISHED', 'JUST_CREATED', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING', 'ERROR', 'CANCELED'" + elsif(status == "SUCCESS") + status = "'FINISHED'" + elsif(status == "WORKING") + status = "'JUST_CREATE', 'WAITING', 'WORKING', 'REMOTE_WORKING', 'PENDING'" + elsif(status == "ERROR") + status = "'ERROR', 'CANCELED'" + end + + if(query_type == "QUERY") + if(last_id == "LATEST") + last_job_id = Job.maximum(:id) + if last_job_id.nil? + last_job_id = 0 + end + else + last_job_id = last_id.to_i - 1 + end + condition = "jobs.id <= #{last_job_id}" + else + condition = "jobs.id > #{last_id}" + end + + project = project + "%" + + jobs = Job.find_by_sql("SELECT jobs.id + , CASE WHEN jobs.jtype like \"MULTI%\" THEN \"MULTI\" + ELSE \"SINGLE\" END \"job_attribute\" + FROM jobs + LEFT JOIN projects + ON jobs.project_id = projects.id + LEFT JOIN users + ON jobs.user_id = users.id + LEFT JOIN supported_os + ON jobs.supported_os_id = supported_os.id + LEFT JOIN distributions + ON jobs.distribution_id = distributions.id + LEFT JOIN remote_build_servers + ON jobs.remote_build_server_id = remote_build_servers.id + LEFT JOIN sources + ON jobs.source_id = sources.id + WHERE #{condition} + AND jobs.status IN (#{status}) + AND projects.name like '#{project}' + ORDER BY jobs.id DESC + LIMIT #{QUERY_CNT}") + + #generate to XML + doc.Data { + doc.JobList { + jobs.each {|job_list| + job = get_job_info(job_list.id) + + doc.Job { + doc.Id(job.job_id) + doc.Distribution(job.distribution_name) + doc.ProjectName(job.project_name) + doc.JobType(job.job_type) + doc.JobAttribute(job_list.job_attribute) + doc.ParentJobId(job.parent_job_id) + doc.Os(job.supported_os_name) + doc.Status(job.status) + doc.UserName(job.user_name) + doc.StartTime(job.start_time) + doc.EndTime(job.end_time) + } + } + } + } + end + + def get_job_info(job_id) + jobs = Job.find_by_sql("SELECT jobs.id AS job_id + , jobs.jtype AS job_type + , jobs.status AS status + , jobs.parent_job_id AS parent_job_id + , DATE_FORMAT(jobs.start_time, '%Y-%m-%d %H:%i:%s') AS start_time + , DATE_FORMAT(jobs.end_time, '%Y-%m-%d %H:%i:%s') AS end_time + , CASE WHEN jobs.jtype like \"MULTI%\" THEN \"MULTI\" + ELSE \"SINGLE\" END \"job_attribute\" + , projects.name AS project_name + , projects.ptype AS project_type + , projects.password AS project_password + , users.name AS user_name + , users.email AS user_email + , supported_os.name AS supported_os_name + , distributions.name AS distribution_name + , distributions.pkgsvr_url AS pkgsvr_url + , distributions.pkgsvr_addr AS pkgsvr_addr + , distributions.description AS distribution_desc + , remote_build_servers.svr_addr AS remote_build_server_addr + , remote_build_servers.description AS remote_build_server_desc + , sources.pkg_ver AS pkg_ver + , sources.location AS location + FROM jobs + LEFT JOIN projects + ON jobs.project_id = projects.id + LEFT JOIN users + ON jobs.user_id = users.id + LEFT JOIN supported_os + ON jobs.supported_os_id = supported_os.id + LEFT JOIN distributions + ON jobs.distribution_id = distributions.id + LEFT JOIN remote_build_servers + ON jobs.remote_build_server_id = remote_build_servers.id + LEFT JOIN sources + ON jobs.source_id = sources.id + WHERE jobs.id = #{job_id}") + return jobs[0] + end + + def get_child_job_info(parent_job_id) + job = Job.find_by_sql("SELECT jobs.id AS job_id + , jobs.jtype AS job_type + , jobs.status AS status + , jobs.parent_job_id AS parent_job_id + , DATE_FORMAT(jobs.start_time, '%Y-%m-%d %H:%i:%s') AS start_time + , DATE_FORMAT(jobs.end_time, '%Y-%m-%d %H:%i:%s') AS end_time + , projects.name AS project_name + , projects.ptype AS project_type + , users.name AS user_name + , users.email AS user_email + , supported_os.name AS supported_os_name + , distributions.name AS distribution_name + , distributions.pkgsvr_url AS pkgsvr_url + , distributions.pkgsvr_addr AS pkgsvr_addr + , distributions.description AS distribution_desc + , remote_build_servers.svr_addr AS remote_build_server_addr + , remote_build_servers.description AS remote_build_server_desc + , sources.pkg_ver AS pkg_ver + , sources.location AS location + FROM jobs + LEFT JOIN projects + ON jobs.project_id = projects.id + LEFT JOIN users + ON jobs.user_id = users.id + LEFT JOIN supported_os + ON jobs.supported_os_id = supported_os.id + LEFT JOIN distributions + ON jobs.distribution_id = distributions.id + LEFT JOIN remote_build_servers + ON jobs.remote_build_server_id = remote_build_servers.id + LEFT JOIN sources + ON jobs.source_id = sources.id + WHERE jobs.parent_job_id = #{parent_job_id}") + return job + end + + def log_more + @cursor = nil + @file = nil + + line_cnt = 999 + conti= 1 + id = params[:id] + puts id + line = params[:line].to_i + + # Get job information + job = get_job_info(id) + if job.nil? + render :text => "Not found Job ID #{id}", :status => 406 + end + + if job.job_type == "MULTIBUILD" + project_name = job.job_type + else + project_name = job.project_name + end + + # Get log file infomation + file_name = "log" + buildsvr_path = Server_config.find(:first, :conditions => ["property = ?", "path"]) + directory = "#{buildsvr_path.value}/jobs/#{id}" + + # create the file path + path = File.join(directory, file_name) + + # read the file + if File.exist?(path) + @cursor = File.size(path) + @file = File.open(path, 'r') + data = @file.readlines + end + + time = Time.new + timestamp = time.getlocal + + start_line = line + last_line = start_line + line_cnt + + #count line + tok = nil + IO.popen("wc -l #{path}") do |wc| + tok = wc.read.split(" ") + end + end_line = tok[0].to_i + + #check line + if last_line > end_line + last_line = end_line + end + + #read log file + temp = nil + if start_line < end_line + IO.popen("sed -n \"#{start_line},#{last_line}p\" #{path}") do |sed| + temp = sed.read + end + else + conti= 0 + end + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Responser { + generate_xml_header(doc) + doc.Data { + doc.JobId(id) + doc.Distribution(job.distribution_name) + doc.Project(project_name) + doc.Builder(job.user_name) + doc.Status(job.status) + doc.Time(timestamp) + doc.Continue(conti) + if temp + temp.each do |data| + doc.LogData(data, "Line" => line) + line = line + 1 + end + end + } + } + + render :text => out_string, :content_type => "text/xml" + end + + def cancelJob + id = params[:id] + + # get job information + job = get_job_info(id) + if job.nil? + render :text => "Not found Job ID #{id}", :status => 406 + return + end + + # Check job builder + if get_user_email != job.user_email + if not is_admin(get_user_email) + render :text => "You don't have authority to calcel.", :status => 406 + return + end + end + + # Excute command + begin + Utils.sbi_cancel_command(id, job.project_password) + rescue => e + render :text => e.message, :status => 406 + return + end + render :xml=> "OK" + end +end diff --git a/dibs-web/app/controllers/projects_controller.rb b/dibs-web/app/controllers/projects_controller.rb new file mode 100644 index 0000000..2bb8c94 --- /dev/null +++ b/dibs-web/app/controllers/projects_controller.rb @@ -0,0 +1,407 @@ +=begin + projects_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'json' + +class ProjectsController < ApplicationController + + layout :myLayout + before_filter :check_login_status + respond_to :json + + def myLayout + params[:action] == 'fileUpload' ? nil : 'application' + end + + + def buildProject + # get information : dist_name, project_hash, password + change_group_list = params[:ChangeInfoList] + + project_list = [] + os_list = [] + password_list = [] + + dist_name = nil + build_type = nil + + change_group_list.each do |change_item| + dist_name = change_item[:distribution] + build_type = change_item[:buildType] + project_name = change_item[:projectName] + os = change_item[:os] + + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + project = Project.find(:first, :conditions => ["name = ? AND distribution_id =?", project_name, distribution.id]) + + project_list.push project.name + password_list.push project.password + os_list.push os + end + + project_list.uniq! + os_list.uniq! + password_list.uniq! + + if (project_list.length > 1) or (not (project_list[0].nil? or project_list[0].empty?)) + # execute build command + begin + Utils.sbi_build_command(build_type, dist_name, project_list, os_list, password_list, get_user_email) + rescue => e + render :text => e.message, :status => 406 + return + end + + render :json => { :success => "OK!" } + else + render :text => "Can't build project", :status => 406 + end + + end + + def queryPackageInfo(project_id, os_id) + + source = Project.find_by_sql("SELECT sources.pkg_ver AS pkg_ver + , DATE_FORMAT(jobs.start_time, '%Y-%m-%d %H:%i:%s') AS start_time + , DATE_FORMAT(jobs.end_time, '%Y-%m-%d %H:%i:%s') AS end_time + , jobs.source_id AS source_id + , jobs.supported_os_id AS supported_os_id + , users.name AS user_name + , users.email AS user_email + FROM projects + , sources + , jobs + , users + WHERE projects.id = sources.project_id + AND jobs.source_id = sources.id + AND jobs.user_id = users.id + AND projects.id = #{project_id} + AND jobs.supported_os_id = #{os_id} + ORDER BY jobs.id DESC") + + + if source.nil? + return nil + else + return source[0] + end + end + + def checkUserAccessProject(user_id, project_id) + row = Project.find_by_sql("SELECT COUNT(*) AS cnt + FROM user_groups + , groups + , group_project_accesses + WHERE user_groups.group_id = groups.id + AND groups.id = group_project_accesses.group_id + AND user_groups.user_id = #{user_id} + AND group_project_accesses.project_id = #{project_id}") + if row[0].cnt > 0 + return TRUE + end + + return FALSE + end + + def queryProjectsInfoInDistribution + dist_name = params[:distribution] + + user_id = get_user_id + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + projects = Project.find(:all, :conditions => ["distribution_id = ?", distribution.id], :order => "name") + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + doc.ProjectList { + projects.each { |project| + doc.Project { + doc.Name(project.name) + doc.Type(project.ptype) + maintainer = User.find(:first, :conditions => ["id = ?", project.user_id]) + if not maintainer.nil? + doc.Maintainer(maintainer.email) + else + doc.Maintainer("") + end + + if checkUserAccessProject(user_id, project.id) + doc.GroupAccess("TRUE") + else + doc.GroupAccess("FALSE") + end + + os_list = ProjectOs.find(:all, :conditions => ["project_id = ?", project.id], :order => "supported_os_id") + os_list.each { |os| + doc.ProjectOs { + os_info = SupportedOs.find(:first, :conditions => ["id = ?", os.supported_os_id]) + doc.OsName(os_info.name) + + source = queryPackageInfo(project.id, os.supported_os_id) + if source.nil? + doc.Package { + doc.PackageName() + doc.PackageVersion() + doc.StartTime() + doc.EndTime() + doc.UserName() + doc.UserEmail() + } + else + packageList = Package.find(:all, :conditions => ["source_id = ? AND supported_os_id = ?", source.source_id, source.supported_os_id]) + + + packageList.each { |package| + doc.Package { + doc.PackageName(package.pkg_name) + doc.PackageVersion(source.pkg_ver) + doc.StartTime(source.start_time) + doc.EndTime(source.end_time) + doc.UserName(source.user_name) + doc.UserEmail(source.user_email) + } + } + end + } + } + } + } + } + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryRunningProjectsInfoInDistribution + dist_name = params[:distribution] + + running_project_list = Project.find_by_sql("SELECT projects.name + , projects.ptype + , jobs.status + , supported_os.name AS os_name + FROM jobs + , projects + , supported_os + , distributions + WHERE jobs.project_id = projects.id + AND distributions.name = \"#{dist_name}\" + AND projects.distribution_id = distributions.id + AND NOT jobs.status in ('FINISHED', 'ERROR', 'CANCELED') + AND supported_os.id = jobs.supported_os_id + ORDER BY jobs.id") + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + if not running_project_list.nil? + running_project_list.each do |run_project| + doc.RunProjectInfo { + doc.ProjectName(run_project.name) + doc.ProjectOs(run_project.os_name) + doc.ProjectType(run_project.ptype) + doc.Status(run_project.status) + } + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def queryJobStatus(project_id, supportes_os_id) + job = Job.find(:first, :conditions => ["status NOT IN ('FINISHED', 'ERROR') AND project_id = ? AND supported_os_id = ?", project_id, supported_os_id, ], :order => "id DESC") + if job.nil? + status = job.status + else + status = nil + end + return status + end + + def queryProjectsInDistribution + dist_name = params[:distribution] + + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + projects = Project.find(:all, :conditions => ["distribution_id = ?", distribution.id], :order => "name") + osList = SupportedOs.find(:all) + + # check can build + project_access_list = [] + + # get all my project + group_list = get_group_list(get_user_email) + + group_id_list = [] + group_list.each { |group| + group_id_list.push group.id + } + + group_name_list_string = + group_access_project_list = GroupProjectAccess.find(:all, :conditions => ["group_id in (?)", group_id_list.join(",") ]) + if not group_access_project_list.nil? + group_access_project_list.each do |access_project| + project_access_list.push access_project.project_id + end + end + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + doc.Data { + doc.BuildServerInfo { + if not osList.nil? + osList.each do |os| + doc.supportedOs(os.name) + end + end + } + + projects.each do |project| + if project.ptype.eql? "BINARY" + bin = ProjectBin.find(:first, :conditions => ["project_id = ?", project.id]) + if project_access_list.include? project.id + doc.BinaryProject { + doc.ProjectName(project.name) + if not bin.nil? then + doc.PackageName(bin.pkg_name) + end + } + else + doc.OtherBinaryProject { + doc.ProjectName(project.name) + if not bin.nil? then + doc.PackageName(bin.pkg_name) + end + } + end + else + buildOsNameList = [] + prjOsLists = ProjectOs.find(:all, :conditions => ["project_id = ?", project.id]) + + prjOsLists.each do |list| + supported_os = SupportedOs.find(:first, :conditions => ["id = ?", list.supported_os_id]) + buildOsNameList.push(supported_os.name) + end + + if project_access_list.include? project.id + doc.Project { + doc.ProjectName(project.name) + doc.OsList(buildOsNameList.join(",")) + } + else + doc.OtherProject { + doc.ProjectName(project.name) + doc.OsList(buildOsNameList.join(",")) + } + end + end + + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + + end + + def queryDistribution + distribution_list = [] + + # get full distribution list + distributions = Distribution.all + distributions.each {|distribution| + distribution_list.push distribution.name + } + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + if not distribution_list.nil? + distribution_list.each do |distribution| + doc.DistributionName(distribution) + end + end + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def projects + end + + def fileUpload + + end + + def binaryFileUpload + dist_name = params[:distribution] + project = params[:project] + uploaded_io = params[:file] + + file_path = Rails.root.join('public', 'data', uploaded_io.original_filename) + File.open(file_path, 'wb') do |file| + file.write(uploaded_io.read) + end + + distribution = Distribution.find(:first, :conditions => ["name = ?", dist_name]) + project = Project.find(:first, :conditions => ["name = ? AND distribution_id =?", project, distribution.id]) + if project.nil? + render :nothing => true + return + end + + begin + Utils.sbi_register_command(distribution.name, file_path, project.password, get_user_email) + rescue => e + render :text => e.message, :status => 406 + return + end + + render :nothing => true + return + + render :text => uploaded_io.original_filename + " is uploaded.", + :content_type => "text/xml" + end + +end diff --git a/dibs-web/app/controllers/sessions_controller.rb b/dibs-web/app/controllers/sessions_controller.rb new file mode 100644 index 0000000..9d90123 --- /dev/null +++ b/dibs-web/app/controllers/sessions_controller.rb @@ -0,0 +1,107 @@ +=begin + sessions_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class SessionsController < ApplicationController + def new + end + + def login + input_list = params[:ChangeInfoList] + input_item = input_list[0] + + create_session(input_item[:Email], input_item[:Password]) + end + + def create_session(email, password) + user = User.authenticate(email, password) + + if user + group_list = get_group_list(user.email) + if is_admin(user.email) + admin = "TRUE" + else + admin = "FALSE" + end + + session[:user_email] = user.email + user_info = { :Message => 'Welcome!', + :Eamil => user.email, + :Name => user.name, + :Admin => admin, + } + group_info = nil + group_list.map { |group| + group_info = {:Group => [:GroupName => group.name, + :GroupAdmin => group.admin, + :GroupDescription => group.description] } + } + + render :json => { :Result => 'SUCCESS', :UserInfo => user_info, :GroupInfo => group_info} + puts user_info + else + render :json => { :Result => 'ERROR', :UserInfo => {:Message => 'Fail login'} } + end + return + end + + def logout + session[:user_email] = nil + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + + doc.LogOutInfo { + doc.Message("Welcome...") + doc.LogInTime("") + doc.LogOutTime("") + } + + render :text => out_string, + :content_type => "text/xml" + end + + def create + user = User.authenticate(params[:email], params[:password]) + + #generate to XML + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + + if user + session[:user_email] = user.email + redirect_to "/projects" + else + flash.now.alert = "Invalid email or password" + render "new" + end + end + + def destroy + session[:user_email] = nil + redirect_to root_url, :notice => "Logged out!" + end +end diff --git a/dibs-web/app/controllers/users_controller.rb b/dibs-web/app/controllers/users_controller.rb new file mode 100644 index 0000000..0b830a1 --- /dev/null +++ b/dibs-web/app/controllers/users_controller.rb @@ -0,0 +1,172 @@ +=begin + users_controller.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class UsersController < ApplicationController + + def new + @user = User.new(params[:user]) + end + + def create + @user = User.new(params[:user]) + + if @user.save + redirect_to "/projects", :notice => "Signed up!" + else + render :action => "new" + end + end + + def signup + change_list = params[:ChangeInfoList] + change_item = change_list[0] + + @user = User.new() + @user.email = change_item[:Email] + @user.name = change_item[:Name] + @user.password = change_item[:Password] + @user.password_confirmation = change_item[:PasswordConfirm] + + if @user.save + create_session(@user.email, @user.password) + else + render :json => { :error => "Error"}, :status => 406 + end + + end + + def create_session(email, password) + user = User.authenticate(email, password) + + if user + group_list = get_group_list(user.email) + if is_admin(user.email) + admin = "TRUE" + else + admin = "FALSE" + end + + session[:user_email] = user.email + user_info = { :Message => 'Welcome!', + :Eamil => user.email, + :Name => user.name, + :Admin => admin, + } + group_info = nil + group_list.map { |group| + group_info = {:Group => [:GroupName => group.name, + :GroupAdmin => group.admin, + :GroupDescription => group.description] } + } + + render :json => { :Result => 'SUCCESS', :UserInfo => user_info, :GroupInfo => group_info} + puts user_info + else + render :json => { :Result => 'ERROR', :UserInfo => {:Message => 'Fail login'} } + end + return + end + + def show + email = get_user_email + user = User.find(:first, :conditions => ["email = ?", email]) + + doc = Builder::XmlMarkup.new( :target => out_string = "", :indent => 2 ) + doc.Response { + generate_xml_header(doc) + + doc.Data { + doc.User { + doc.Name(user.name) + doc.Email(user.email) + doc.GroupList { + group_list = get_group_list(user.email) + group_list.each { |group| + doc.GroupName(group.name) + } + } + } + } + } + + #send_data + render :text => out_string, :content_type => "text/xml" + end + + def modify + change_list = params[:ChangeInfoList] + change_item = change_list[0] + errmsg = "" + + email = change_item[:Email] + name = change_item[:Name] + password = change_item[:Password] + password_confirm = change_item[:PasswordConfirm] + + if email != get_user_email + errmsg = "Can't modify email" + render :json => { :error => errmsg }, :status => 406 + return + end + + if email.nil? or email.empty? + errmsg = "Email is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + if name.nil? or name.empty? + errmsg = "Name is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + if password.nil? or password.empty? + errmsg = "Password is empty" + render :json => { :error => errmsg }, :status => 406 + return + end + + if password != password_confirm + errmsg = "Password is different" + render :json => { :error => errmsg }, :status => 406 + return + end + user = User.find(:first, :conditions => ["email = ?", email]) + user.name = name + user.password = password + user.password_confirmation = password_confirm + user.save + + if errmsg.empty? + render :json => { :success => "OK!" } + else + render :json => { :error => errmsg }, :status => 406 + end + end +end diff --git a/dibs-web/app/controllers/utils.rb b/dibs-web/app/controllers/utils.rb new file mode 100644 index 0000000..b0e5f4b --- /dev/null +++ b/dibs-web/app/controllers/utils.rb @@ -0,0 +1,130 @@ +=begin + utils.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +# constant +BUILD_SERVER_ADDRESS = "127.0.0.1" + +class Utils < ApplicationController + def Utils.sbi_build_command(sub_cmd, distribution, project_list, os_list, password_list, email) + dibs_path = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) + dibs_config = Server_config.find(:first, :conditions => ["property = \"port\""]) + if dibs_config.nil? + raise RuntimeError, "Build sever not started" + else + dibs_address = BUILD_SERVER_ADDRESS + ":" + dibs_config.value + end + + options = "-d #{dibs_address} --async " + options = options + " -N #{project_list.join(",")} -D #{distribution} " + if (os_list.length > 1) or (not os_list[0].nil?) + options = options + " -o #{os_list.join(",")} " + end + if (password_list.length > 1) or (not (password_list[0].nil? or password_list[0].empty?)) + options = options + " -w #{password_list.join(",")} " + end + if not email.nil? + options = options + " -U #{email} " + end + + cmd = "#{dibs_path}/build-cli #{sub_cmd} #{options}" +puts "Build command" +puts "[[[#{cmd}]]]" + + return execute_shell_return(cmd) + end + + def Utils.sbi_register_command(distribution, file_path, password, email) + dibs_path = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) + dibs_config = Server_config.find(:first, :conditions => ["property = \"port\""]) + if dibs_config.nil? + raise RuntimeError, "Build sever not started" + else + dibs_address = BUILD_SERVER_ADDRESS + ":" + dibs_config.value + end + + options = "-P #{file_path} -d #{dibs_address} -D #{distribution} --async " + if (not password.nil?) + options = options + " -w #{password}" + end + if not email.nil? + options = options + " -U #{email} " + end + + cmd = "#{dibs_path}/build-cli register #{options}" +puts "Register command" +puts "[[[#{cmd}]]]" + return execute_shell_return(cmd) + end + + def Utils.sbi_cancel_command(job_id, password) + dibs_path = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) + dibs_config = Server_config.find(:first, :conditions => ["property = \"port\""]) + if dibs_config.nil? + raise RuntimeError, "Build sever not started" + else + dibs_address = BUILD_SERVER_ADDRESS + ":" + dibs_config.value + end + + if job_id.nil? + raise RuntimeError, "Invalid job id : #{job_id}" + end + + options = "-d #{dibs_address} " + options = options + " -j #{job_id}" + if (not password.nil?) + options = options + " -w #{password}" + end + + cmd = "#{dibs_path}/build-cli cancel #{options}" +puts "Cancel command" +puts "[[[#{cmd}]]]" + return execute_shell_return(cmd) + end + + def Utils.sbi_fullbuild_command(server_name, dist_name) + dibs_path = File.dirname(File.dirname(File.dirname(File.dirname(__FILE__)))) + + options = "-n #{server_name} --dist #{dist_name} " + cmd = "#{dibs_path}/build-svr fullbuild #{options}" +puts "Fullbuild command" +puts "[[[#{cmd}]]]" + return execute_shell_return(cmd) + end + + def Utils.execute_shell_return(cmd) + result_lines = [] + ret = false + + # get result + IO.popen("#{cmd}") + #system "#{cmd}" + + return true + end + +end diff --git a/dibs-web/app/helpers/admin_distribution_helper.rb b/dibs-web/app/helpers/admin_distribution_helper.rb new file mode 100644 index 0000000..97c39f6 --- /dev/null +++ b/dibs-web/app/helpers/admin_distribution_helper.rb @@ -0,0 +1,2 @@ +module AdminDistributionHelper +end diff --git a/dibs-web/app/helpers/admin_group_helper.rb b/dibs-web/app/helpers/admin_group_helper.rb new file mode 100644 index 0000000..9561d3b --- /dev/null +++ b/dibs-web/app/helpers/admin_group_helper.rb @@ -0,0 +1,2 @@ +module AdminGroupHelper +end diff --git a/dibs-web/app/helpers/admin_helper.rb b/dibs-web/app/helpers/admin_helper.rb new file mode 100644 index 0000000..d5c6d35 --- /dev/null +++ b/dibs-web/app/helpers/admin_helper.rb @@ -0,0 +1,2 @@ +module AdminHelper +end diff --git a/dibs-web/app/helpers/admin_project_helper.rb b/dibs-web/app/helpers/admin_project_helper.rb new file mode 100644 index 0000000..7925981 --- /dev/null +++ b/dibs-web/app/helpers/admin_project_helper.rb @@ -0,0 +1,2 @@ +module AdminProjectHelper +end diff --git a/dibs-web/app/helpers/admin_server_helper.rb b/dibs-web/app/helpers/admin_server_helper.rb new file mode 100644 index 0000000..5c7d70a --- /dev/null +++ b/dibs-web/app/helpers/admin_server_helper.rb @@ -0,0 +1,2 @@ +module AdminServerHelper +end diff --git a/dibs-web/app/helpers/admin_user_helper.rb b/dibs-web/app/helpers/admin_user_helper.rb new file mode 100644 index 0000000..3edd72d --- /dev/null +++ b/dibs-web/app/helpers/admin_user_helper.rb @@ -0,0 +1,2 @@ +module AdminUserHelper +end diff --git a/dibs-web/app/helpers/application_helper.rb b/dibs-web/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/dibs-web/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/dibs-web/app/helpers/jobs_helper.rb b/dibs-web/app/helpers/jobs_helper.rb new file mode 100644 index 0000000..44c7bf6 --- /dev/null +++ b/dibs-web/app/helpers/jobs_helper.rb @@ -0,0 +1,2 @@ +module JobsHelper +end diff --git a/dibs-web/app/helpers/projects_helper.rb b/dibs-web/app/helpers/projects_helper.rb new file mode 100644 index 0000000..fc47f60 --- /dev/null +++ b/dibs-web/app/helpers/projects_helper.rb @@ -0,0 +1,4 @@ +module ProjectsHelper + def update_text(name) + end +end diff --git a/dibs-web/app/helpers/sessions_helper.rb b/dibs-web/app/helpers/sessions_helper.rb new file mode 100644 index 0000000..309f8b2 --- /dev/null +++ b/dibs-web/app/helpers/sessions_helper.rb @@ -0,0 +1,2 @@ +module SessionsHelper +end diff --git a/dibs-web/app/helpers/users_helper.rb b/dibs-web/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/dibs-web/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/dibs-web/app/mailers/.gitkeep b/dibs-web/app/mailers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dibs-web/app/models/.gitkeep b/dibs-web/app/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/dibs-web/app/models/distribution.rb b/dibs-web/app/models/distribution.rb new file mode 100644 index 0000000..994dc84 --- /dev/null +++ b/dibs-web/app/models/distribution.rb @@ -0,0 +1,3 @@ +class Distribution < ActiveRecord::Base + attr_accessible :id, :name, :pkgsvr_url, :pkgsvr_addr, :status, :description +end diff --git a/dibs-web/app/models/group.rb b/dibs-web/app/models/group.rb new file mode 100644 index 0000000..55595be --- /dev/null +++ b/dibs-web/app/models/group.rb @@ -0,0 +1,5 @@ +class Group < ActiveRecord::Base +# has_many :user_groups +# has_many :users, :through => :user_groups + attr_accessible :name, :id +end diff --git a/dibs-web/app/models/group_project_access.rb b/dibs-web/app/models/group_project_access.rb new file mode 100644 index 0000000..dee4f5e --- /dev/null +++ b/dibs-web/app/models/group_project_access.rb @@ -0,0 +1,4 @@ +class GroupProjectAccess < ActiveRecord::Base + self.primary_key = 'group_id', 'project_id' + attr_accessible :group_id, :project_id, :build +end diff --git a/dibs-web/app/models/job.rb b/dibs-web/app/models/job.rb new file mode 100644 index 0000000..3c15994 --- /dev/null +++ b/dibs-web/app/models/job.rb @@ -0,0 +1,3 @@ +class Job < ActiveRecord::Base + attr_accessible :distribution_id, :id, :supported_os_id, :project_id, :status, :user_id, :remote_build_server_id, :parent_job_id, :source_id, :jtype, :start_time, :end_time +end diff --git a/dibs-web/app/models/os_category.rb b/dibs-web/app/models/os_category.rb new file mode 100644 index 0000000..a8fbee1 --- /dev/null +++ b/dibs-web/app/models/os_category.rb @@ -0,0 +1,4 @@ +class OsCategory < ActiveRecord::Base + set_table_name "os_category" + attr_accessible :id, :name +end diff --git a/dibs-web/app/models/package.rb b/dibs-web/app/models/package.rb new file mode 100644 index 0000000..7e6b44f --- /dev/null +++ b/dibs-web/app/models/package.rb @@ -0,0 +1,3 @@ +class Package < ActiveRecord::Base + attr_accessible :id, :pkg_name, :source_id, :supported_os_id +end diff --git a/dibs-web/app/models/project.rb b/dibs-web/app/models/project.rb new file mode 100644 index 0000000..1c9d56e --- /dev/null +++ b/dibs-web/app/models/project.rb @@ -0,0 +1,3 @@ +class Project < ActiveRecord::Base + attr_accessible :id, :distribution_id, :name, :ptype, :password +end diff --git a/dibs-web/app/models/project_bin.rb b/dibs-web/app/models/project_bin.rb new file mode 100644 index 0000000..3252b0c --- /dev/null +++ b/dibs-web/app/models/project_bin.rb @@ -0,0 +1,3 @@ +class ProjectBin < ActiveRecord::Base + attr_accessible :project_id, :pkg_name +end diff --git a/dibs-web/app/models/project_git.rb b/dibs-web/app/models/project_git.rb new file mode 100644 index 0000000..c0a5d4d --- /dev/null +++ b/dibs-web/app/models/project_git.rb @@ -0,0 +1,3 @@ +class ProjectGit < ActiveRecord::Base + attr_accessible :project_id, :git_repos, :git_branch +end diff --git a/dibs-web/app/models/project_os.rb b/dibs-web/app/models/project_os.rb new file mode 100644 index 0000000..670dab0 --- /dev/null +++ b/dibs-web/app/models/project_os.rb @@ -0,0 +1,4 @@ +class ProjectOs < ActiveRecord::Base + self.primary_key = 'project_id', 'supported_os_id' + attr_accessible :project_id, :supported_os_id +end diff --git a/dibs-web/app/models/remote_build_server.rb b/dibs-web/app/models/remote_build_server.rb new file mode 100644 index 0000000..b02b1e5 --- /dev/null +++ b/dibs-web/app/models/remote_build_server.rb @@ -0,0 +1,3 @@ +class RemoteBuildServer < ActiveRecord::Base + attr_accessible :id, :svr_addr, :description, :status, :max_job_count, :working_job_count, :waiting_job_count +end diff --git a/dibs-web/app/models/server_config.rb b/dibs-web/app/models/server_config.rb new file mode 100644 index 0000000..68f2df8 --- /dev/null +++ b/dibs-web/app/models/server_config.rb @@ -0,0 +1,3 @@ +class Server_config < ActiveRecord::Base + attr_accessible :property, :value +end diff --git a/dibs-web/app/models/source.rb b/dibs-web/app/models/source.rb new file mode 100644 index 0000000..f8bd214 --- /dev/null +++ b/dibs-web/app/models/source.rb @@ -0,0 +1,3 @@ +class Source < ActiveRecord::Base + attr_accessible :id, :location, :pkg_ver, :project_id +end diff --git a/dibs-web/app/models/supported_os.rb b/dibs-web/app/models/supported_os.rb new file mode 100644 index 0000000..c37b19f --- /dev/null +++ b/dibs-web/app/models/supported_os.rb @@ -0,0 +1,3 @@ +class SupportedOs < ActiveRecord::Base + attr_accessible :id, :os_category_id, :name +end diff --git a/dibs-web/app/models/sync_pkg_server.rb b/dibs-web/app/models/sync_pkg_server.rb new file mode 100644 index 0000000..0863806 --- /dev/null +++ b/dibs-web/app/models/sync_pkg_server.rb @@ -0,0 +1,3 @@ +class SyncPkgServer < ActiveRecord::Base + attr_accessible :description, :distribution_id, :id, :period, :pkgsvr_url +end diff --git a/dibs-web/app/models/sync_project.rb b/dibs-web/app/models/sync_project.rb new file mode 100644 index 0000000..b860a7a --- /dev/null +++ b/dibs-web/app/models/sync_project.rb @@ -0,0 +1,3 @@ +class SyncProject < ActiveRecord::Base + attr_accessible :project_id, :sync_pkg_server_id +end diff --git a/dibs-web/app/models/user.rb b/dibs-web/app/models/user.rb new file mode 100644 index 0000000..ebfb5f1 --- /dev/null +++ b/dibs-web/app/models/user.rb @@ -0,0 +1,29 @@ +class User < ActiveRecord::Base +# has_many :user_groups +# has_many :groups, :through => :user_groups + attr_accessible :id, :email, :password, :password_confirmation, :name + + attr_accessor :password + before_save :encrypt_password + + validates_uniqueness_of :email + validates_presence_of :email + validates_confirmation_of :password + validates_presence_of :password, :on => :create + + def self.authenticate(email, password) + user = find_by_email(email) + if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt) + user + else + nil + end + end + + def encrypt_password + if password.present? + self.password_salt = BCrypt::Engine.generate_salt + self.password_hash = BCrypt::Engine.hash_secret(password, password_salt) + end + end +end diff --git a/dibs-web/app/models/user_group.rb b/dibs-web/app/models/user_group.rb new file mode 100644 index 0000000..d963db5 --- /dev/null +++ b/dibs-web/app/models/user_group.rb @@ -0,0 +1,7 @@ +class UserGroup < ActiveRecord::Base +# belongs_to :users +# belongs_to :groups + self.primary_key = 'group_id' + self.primary_key = 'user_id' + attr_accessible :group_id, :status, :user_id +end diff --git a/dibs-web/config.ru b/dibs-web/config.ru new file mode 100644 index 0000000..536e8d2 --- /dev/null +++ b/dibs-web/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Dibs::Application diff --git a/dibs-web/config/application.rb b/dibs-web/config/application.rb new file mode 100644 index 0000000..1e8680c --- /dev/null +++ b/dibs-web/config/application.rb @@ -0,0 +1,62 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(:assets => %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) +end + +module Dibs + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Custom directories with classes and modules you want to be autoloadable. + # config.autoload_paths += %W(#{config.root}/extras) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # Enable escaping HTML in JSON. + config.active_support.escape_html_entities_in_json = true + + # Use SQL instead of Active Record's schema dumper when creating the database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Enforce whitelist mode for mass assignment. + # This will create an empty whitelist of attributes available for mass-assignment for all models + # in your app. As such, your models will need to explicitly whitelist or blacklist accessible + # parameters by using an attr_accessible or attr_protected declaration. + config.active_record.whitelist_attributes = true + + # Enable the asset pipeline + config.assets.enabled = true + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + end +end diff --git a/dibs-web/config/boot.rb b/dibs-web/config/boot.rb new file mode 100644 index 0000000..4489e58 --- /dev/null +++ b/dibs-web/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/dibs-web/config/database.yml b/dibs-web/config/database.yml new file mode 100644 index 0000000..319f61e --- /dev/null +++ b/dibs-web/config/database.yml @@ -0,0 +1,46 @@ +# SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# development: +# adapter: sqlite3 +# database: db/development.sqlite3 +# pool: 5 +# timeout: 5000 +# +# # Warning: The database defined as "test" will be erased and +# # re-generated from your development database when you run "rake". +# # Do not set this db to the same as development or production. + test: + adapter: + encoding: + host: + port: + database: + username: + password: + pool: + timeout: + + production: + adapter: + encoding: + host: + port: + database: + username: + password: + pool: + timeout: + + development: + adapter: + encoding: + host: + port: + database: + username: + password: + pool: + timeout: diff --git a/dibs-web/config/environment.rb b/dibs-web/config/environment.rb new file mode 100644 index 0000000..b3360a8 --- /dev/null +++ b/dibs-web/config/environment.rb @@ -0,0 +1,7 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +Dibs::Application.initialize! + +Rails.logger = Logger.new(STDOUT) diff --git a/dibs-web/config/environments/development.rb b/dibs-web/config/environments/development.rb new file mode 100644 index 0000000..f2b6b9b --- /dev/null +++ b/dibs-web/config/environments/development.rb @@ -0,0 +1,37 @@ +Dibs::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + config.active_record.auto_explain_threshold_in_seconds = 0.5 + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = true +end diff --git a/dibs-web/config/environments/production.rb b/dibs-web/config/environments/production.rb new file mode 100644 index 0000000..9fb3183 --- /dev/null +++ b/dibs-web/config/environments/production.rb @@ -0,0 +1,67 @@ +Dibs::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = true + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = false + + # Generate digests for assets URLs + config.assets.digest = true + + # Defaults to nil and saved in location specified by config.assets.prefix + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # See everything in the log (default is :info) + # config.log_level = :debug + + # Prepend all log lines with the following tags + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile += %w( search.js ) + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 +end diff --git a/dibs-web/config/environments/test.rb b/dibs-web/config/environments/test.rb new file mode 100644 index 0000000..e6b8932 --- /dev/null +++ b/dibs-web/config/environments/test.rb @@ -0,0 +1,37 @@ +Dibs::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr +end diff --git a/dibs-web/config/initializers/backtrace_silencers.rb b/dibs-web/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..59385cd --- /dev/null +++ b/dibs-web/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/dibs-web/config/initializers/inflections.rb b/dibs-web/config/initializers/inflections.rb new file mode 100644 index 0000000..5d8d9be --- /dev/null +++ b/dibs-web/config/initializers/inflections.rb @@ -0,0 +1,15 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format +# (all these examples are active by default): +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end +# +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/dibs-web/config/initializers/mime_types.rb b/dibs-web/config/initializers/mime_types.rb new file mode 100644 index 0000000..72aca7e --- /dev/null +++ b/dibs-web/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register_alias "text/html", :iphone diff --git a/dibs-web/config/initializers/secret_token.rb b/dibs-web/config/initializers/secret_token.rb new file mode 100644 index 0000000..d99b878 --- /dev/null +++ b/dibs-web/config/initializers/secret_token.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +Dibs::Application.config.secret_token = '7971db0f7470719db67c53a48ec8906f77b093334897b37d3c7de71298f18d1b4c92d56a8e158884f656ad4620f495abdef4f1a00b10498d8fde21a396c7171a' diff --git a/dibs-web/config/initializers/session_store.rb b/dibs-web/config/initializers/session_store.rb new file mode 100644 index 0000000..21ce5e6 --- /dev/null +++ b/dibs-web/config/initializers/session_store.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +Dibs::Application.config.session_store :cookie_store, :key => '_dibs_session' + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rails generate session_migration") +# Dibs::Application.config.session_store :active_record_store diff --git a/dibs-web/config/initializers/wrap_parameters.rb b/dibs-web/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..da4fb07 --- /dev/null +++ b/dibs-web/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters :format => [:json] +end + +# Disable root element in JSON by default. +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false +end diff --git a/dibs-web/config/locales/en.yml b/dibs-web/config/locales/en.yml new file mode 100644 index 0000000..179c14c --- /dev/null +++ b/dibs-web/config/locales/en.yml @@ -0,0 +1,5 @@ +# Sample localization file for English. Add more files in this directory for other locales. +# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en: + hello: "Hello world" diff --git a/dibs-web/config/routes.rb b/dibs-web/config/routes.rb new file mode 100644 index 0000000..5d0385e --- /dev/null +++ b/dibs-web/config/routes.rb @@ -0,0 +1,179 @@ +=begin + routes.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +Dibs::Application.routes.draw do + root :to => "sessions#new" + + get "sessions/new" => "sessions#new" + + resources :users + + get "jobs" => "jobs#list" + + get "projects" => "projects#projects" + get "projects/fileUpload" + + get "signup" => "users#new", :as => "signup" + + post "login" => "sessions#create" + get "logout" => "sessions#destroy", :as => "logout" + + + # For asynchronous call + + post "users/signup" + post "users/modify" + + post "sessions/login" + delete "sessions/logout" + + get "jobs/list/all/:distribution/:status/:lastID" => "jobs#listAll", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + get "jobs/list/user/:user/:distribution/:status/:lastID" => "jobs#listSearchUser", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + get "jobs/list/group/:group/:distribution/:status/:lastID" => "jobs#listSearchGroup", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + get "jobs/list/project/:project/:distribution/:status/:lastID" => "jobs#listSearchProject" , :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/, :project => /[0-9A-Za-z\-\.]+/ } + get "jobs/list/date/:date/:distribution/:status/:lastID" => "jobs#listSearchDate", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + + match "jobs/update" => "jobs#updateList", :format => "json" + match "jobs/log/:id" => "jobs#log" + match "jobs/log/:id/:line" => "jobs#log_more" + match "jobs/cancel/:id" => "jobs#cancelJob" + + + get "projects/queryDistribution" + match "projects/queryProject/:distribution" => "projects#queryProject", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + post "projects/binaryFileUpload" + + # projects + match "projects/queryRunningProjectsInfoInDistribution/:distribution" => "projects#queryRunningProjectsInfoInDistribution", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + match "projects/queryProjectsInfoInDistribution/:distribution" => "projects#queryProjectsInfoInDistribution", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + match "projects/queryProjectsInDistribution/:distribution" => "projects#queryProjectsInDistribution" , :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + post "projects/buildProject" + + # admin group + get "admin_group/queryAllGroup" + match "admin_group/queryGroupInfo/:groupName" => "admin_group#queryGroupInfo" + post "admin_group/addGroup" + post "admin_group/removeGroup" + post "admin_group/modifyGroup" + + # admin user + get "admin_user/queryAllUser" + post "admin_user/removeUser" + post "admin_user/modifyUser" + + # admin server + get "admin_server/queryAllServer" + post "admin_server/addRemoteBuildServer" + post "admin_server/removeRemoteBuildServer" + post "admin_server/modifyRemoteBuildServer" + post "admin_server/addOsCategory" + post "admin_server/removeOsCategory" + post "admin_server/modifyOsCategory" + post "admin_server/addSupportedOS" + post "admin_server/removeSupportedOS" + post "admin_server/modifySupportedOS" + post "admin_server/addServerInfo" + post "admin_server/removeServerInfo" + post "admin_server/modifyServerInfo" + + # admin project + get "admin_project/queryAllProject" + match "admin_project/queryProjectsInDistributionForAdmin/:distribution" => "admin_project#queryProjectsInDistributionForAdmin", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + post "admin_project/addProject" + post "admin_project/removeProject" + post "admin_project/modifyProject" + + # admin distribution + get "admin_distribution/queryAllDistribution" + match "admin_distribution/queryDistributionInfo/:distribution" => "admin_distribution#queryDistributionInfo", :constraints => { :distribution => /[0-9A-Za-z\-\.\_]+/ } + post "admin_distribution/addDistribution" + post "admin_distribution/removeDistribution" + post "admin_distribution/modifyDistribution" + + post "admin_distribution/fullBuildDistribution" + + get "admin/queryAllOS" + get "admin/queryAllOSCategory" + + # The priority is based upon order of creation: + # first created -> highest priority. + + # Sample of regular route: + # match 'products/:id' => 'catalog#view' + # Keep in mind you can assign values other than :controller and :action + + # Sample of named route: + # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase + # This route can be invoked with purchase_url(:id => product.id) + + # Sample resource route (maps HTTP verbs to controller actions automatically): + # resources :products + + # Sample resource route with options: + # resources :products do + # member do + # get 'short' + # post 'toggle' + # end + # + # collection do + # get 'sold' + # end + # end + + # Sample resource route with sub-resources: + # resources :products do + # resources :comments, :sales + # resource :seller + # end + + # Sample resource route with more complex sub-resources + # resources :products do + # resources :comments + # resources :sales do + # get 'recent', :on => :collection + # end + # end + + # Sample resource route within a namespace: + # namespace :admin do + # # Directs /admin/products/* to Admin::ProductsController + # # (app/controllers/admin/products_controller.rb) + # resources :products + # end + + # You can have the root of your site routed with "root" + # just remember to delete public/index.html. + # root :to => 'welcome#index' + + # See how all your routes lay out with "rake routes" + + # This is a legacy wild controller route that's not recommended for RESTful applications. + # Note: This route will make all actions in every controller accessible via GET requests. + # match ':controller(/:action(/:id))(.:format)' +end diff --git a/dibs-web/db/schema.rb b/dibs-web/db/schema.rb new file mode 100644 index 0000000..f0ab07e --- /dev/null +++ b/dibs-web/db/schema.rb @@ -0,0 +1,76 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 20121024015858) do + + create_table "distributions", :force => true do |t| + t.string "name" + t.string "pkgsvr_url" + t.string "pkgsvr_addr" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "group_rights", :force => true do |t| + t.integer "group_id" + t.integer "project_id" + t.string "build" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "groups", :force => true do |t| + t.string "group_name" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "jobs", :force => true do |t| + t.string "job_id", :null => false + t.string "project_name" + t.string "distribution" + t.string "job_attribute" + t.string "os" + t.string "status" + t.integer "user_id" + t.string "parent_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "projects", :force => true do |t| + t.string "project_name" + t.string "distribution_name" + t.string "project_type" + t.string "os_list" + t.string "password" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "supported_os", :force => true do |t| + t.string "name" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "users", :force => true do |t| + t.string "email" + t.string "user_name" + t.string "password_hash" + t.string "password_salt" + t.integer "group_id" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + +end diff --git a/dibs-web/db/seeds.rb b/dibs-web/db/seeds.rb new file mode 100644 index 0000000..d34dfa0 --- /dev/null +++ b/dibs-web/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# +# Examples: +# +# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }]) +# Mayor.create(:name => 'Emanuel', :city => cities.first) diff --git a/dibs-web/doc/README_FOR_APP b/dibs-web/doc/README_FOR_APP new file mode 100644 index 0000000..fe41f5c --- /dev/null +++ b/dibs-web/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/dibs-web/public/.project b/dibs-web/public/.project new file mode 100644 index 0000000..acb2d68 --- /dev/null +++ b/dibs-web/public/.project @@ -0,0 +1,57 @@ + + + dibs + + + + + + org.eclipse.wst.jsdt.core.javascriptValidator + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + json.validation.builder + + + + + org.tizen.web.jslint.nature.JSLintBuilder + + + + + org.tizen.web.css.nature.CSSBuilder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.tizen.web.project.builder.WebBuilder + + + usedLibraryType + jQueryMobile + + + + + + json.validation.nature + org.tizen.web.jslint.nature.JSLintNature + org.tizen.web.css.nature.CSSNature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.wst.jsdt.core.jsNature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.tizen.web.project.builder.WebNature + + diff --git a/dibs-web/public/404.html b/dibs-web/public/404.html new file mode 100644 index 0000000..9a48320 --- /dev/null +++ b/dibs-web/public/404.html @@ -0,0 +1,26 @@ + + + + The page you were looking for doesn't exist (404) + + + + + +
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+ + diff --git a/dibs-web/public/422.html b/dibs-web/public/422.html new file mode 100644 index 0000000..83660ab --- /dev/null +++ b/dibs-web/public/422.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (422) + + + + + +
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+ + diff --git a/dibs-web/public/500.html b/dibs-web/public/500.html new file mode 100644 index 0000000..f3648a0 --- /dev/null +++ b/dibs-web/public/500.html @@ -0,0 +1,25 @@ + + + + We're sorry, but something went wrong (500) + + + + + +
+

We're sorry, but something went wrong.

+
+ + diff --git a/dibs-web/public/config.xml b/dibs-web/public/config.xml new file mode 100644 index 0000000..4ff99b6 --- /dev/null +++ b/dibs-web/public/config.xml @@ -0,0 +1,7 @@ + + + + + + test + diff --git a/dibs-web/public/favicon.ico b/dibs-web/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/dibs-web/public/index.html b/dibs-web/public/index.html new file mode 100644 index 0000000..e132087 --- /dev/null +++ b/dibs-web/public/index.html @@ -0,0 +1,1017 @@ + + + + + + DIBS 2.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

Welcome to DIBS

+
    +
  • This system is support to build project. +
+
+ +
+
    +
+
+
+
+ +
+
+

Log in

+
+
+
+ + + + +
+
+ Cancel + Log in +
+
+
+
+ +
+
+

Sign up

+
+
+
+ + + + + + + + +
+
+
+ Cancel + Sign up +
+
+

+

+
+
+ +
+
+

+
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + + + + + +
+
+
+
+
+
+ +
+
    +
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+
+
+ +
+
+
+

Git project

+
+ Build + Resolve +
+
+ +
+
+
+
+

Binary project

+
+ +
+
+
+ +
+ +
+
    +
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+
+
+
+
+
+ + + + + + + + + + + +
+ +
+
+
+ +
+ + Search + +
    +
+
+
+ + Search +
+
+ +
    +
+
+ + +
+ +
+
    +
+
+
+
+ +
+ +
+
+ + +
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

User information

+
+ +
+
+
+ +
+
    +
+
+
+
+ +
+
+

Modify User

+
+
+
+ + +
+ + +
+
+ +
+ +
+
+
+
+
+ Save +
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

Group information

+ +
+ +
+
+
+ +
+
    +
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

Server information

+ +
+

Server info

+
    +
+
+ +
+

Supported OS

+
    +
+
+ +
+

Remote build server

+
    +
+
+
+
+
    +
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

Project information

+
+
+ +
+
+
+
+
+

Git project

+ +
+ +
+
+
+
+

Binary project

+ + +
+
+
+
+
+ +
+
    +
+
+
+
+ +
+
+ Home +

DIBS 2.0

+
+
+
+ +
+
+

Distribution information

+
+
+ +
+ +
+

Package server url

+

+
+
+

Package server address

+

+
+
+

Status

+

+
+
+

Description

+

+
+
+

Sync package server

+
+
+
+
+ +
+
+ +
+
    +
+
+
+
+ +
+
+

Add Distribution

+
+
+
+ + +
+ + +
+ + +
+ + +
+ + +
+
+

Sync package server

+ + + + + + +
+
+
+
+ Save +
+
+ +
+
+

Modofy Distribution

+
+
+
+ + +
+ + +
+ + +
+ + +
+
+

Sync package server

+ + + + + + +
+
+
+
+ Save +
+
+ +
+
+

Add Git Project

+
+
+
+ + +
+ + +
+ + +
+ + +
+
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

Add Binary Project

+
+
+
+ + +
+ + +
+ + +
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

Modify Project

+
+
+
+ +
+ + +
+
+ +
+ +
+
+ + +
+ + +
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

Modify Project

+
+
+
+ +
+ + +
+
+ +
+ +
+
+ + +
+ + +
+ + +
+
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

Add Group

+
+
+
+ + +
+
+ +
+ +
+
+ + +
+
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

modify Group

+
+
+
+ + + +
+
+ +
+ +
+
+ + +
+
+
+
+
+
+
+
+
+ Save +
+
+ +
+
+

Add remote build server

+
+
+
+ + +
+ + +
+
+
+ Save +
+
+ +
+

Modify server

+
+
+
+ +
+ + +
+ + +
+
+
+ Save + Delete +
+
+ +
+
+

User information

+
+
+
+ + +
+ + +
+ + +
+ + +
+
+
+ Save +
+
+ +
+
+

Add supported os

+
+
+
+ + +
+
+ +
+ +
+
+
+
+ Save +
+
+ +
+
+

Modify supported os

+
+
+
+ + + +
+
+ +
+ +
+
+
+
+ Remove + Save +
+
+ +
+
+

Add os category

+
+
+
+ + +
+
+
+ Save +
+
+ +
+
+

Add os category

+
+
+
+
+ +
+ +
+
+
+
+ Delete +
+
+ +
+

Add server config

+
+
+
+ + +
+ + +
+
+
+ Save +
+
+ +
+

Modify server config

+
+
+
+ + +
+ + +
+
+
+ Save + Delete +
+
+ + + + + diff --git a/dibs-web/public/javascripts/admin-distribution-add.js b/dibs-web/public/javascripts/admin-distribution-add.js new file mode 100644 index 0000000..f648638 --- /dev/null +++ b/dibs-web/public/javascripts/admin-distribution-add.js @@ -0,0 +1,62 @@ +/* + admin-distribution-add.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminDistributionAdd() { + var changeInfoList = []; + var changeInfoItem; + var name = document.getElementById("adminDistributionAddPopup-DistirubtionName").value; + var url = document.getElementById("adminDistributionAddPopup-PackageServerUrl").value; + var address = document.getElementById("adminDistributionAddPopup-PackageServerAddress").value; + var description = document.getElementById("adminDistributionAddPopup-DistributionDescription").value; + var distStatus = $("#adminDistributionAddPopup-DistributionStatus option:selected").val(); + + var sync_pkg_svr_url = document.getElementById("adminDistributionAddPopup-SyncPackageServer-Url").value; + var sync_pkg_svr_period = document.getElementById("adminDistributionAddPopup-SyncPackageServer-period").value; + var sync_pkg_svr_description = document.getElementById("adminDistributionAddPopup-SyncPackageServer-Description").value; + + if(name == "" || url == "" || address == ""){ + alert("You must input full data"); + return; + } + + changeInfoItem = {"DistributionName":name, "URL":url, "Address":address, "DistStatus":distStatus, "Description":description, "SyncPkgSvrUrl":sync_pkg_svr_url, "SyncPkgSvrPeriod":sync_pkg_svr_period, "SyncPkgSvrDescription":sync_pkg_svr_description}; + changeInfoList.push(changeInfoItem); + + addDistribution(changeInfoList, function () { + document.getElementById("adminDistributionAddPopup-DistirubtionName").value = ""; + document.getElementById("adminDistributionAddPopup-PackageServerUrl").value = ""; + document.getElementById("adminDistributionAddPopup-PackageServerAddress").value = ""; + document.getElementById("adminDistributionAddPopup-DistributionDescription").value = ""; + document.getElementById("adminDistributionAddPopup-SyncPackageServer-Url").value = ""; + document.getElementById("adminDistributionAddPopup-SyncPackageServer-period").value = ""; + document.getElementById("adminDistributionAddPopup-SyncPackageServer-Description").value = ""; + + $.mobile.changePage("#adminDistribution"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-distribution-modify.js b/dibs-web/public/javascripts/admin-distribution-modify.js new file mode 100644 index 0000000..5c5c293 --- /dev/null +++ b/dibs-web/public/javascripts/admin-distribution-modify.js @@ -0,0 +1,110 @@ +/* + admin-distribution-modify.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminDistributionModifyPopupInit() { + var distName = $("#adminDistributionSelect option:selected").val(); + var packageServerUrl = document.getElementById("adminDistribution:packageServerUrl").innerHTML; + var packageServeraddress = document.getElementById("adminDistribution:packageServerAddress").innerHTML; + var serverStatusText = document.getElementById("adminDistribution:distributionStatus").innerHTML; + var serverDescription = document.getElementById("adminDistribution:distributionDescription").innerHTML; + + var syncPkgSvrUrl = document.getElementById("adminDistribution-SyncPackageServer-Url").innerHTML; + var syncPkgSvrPeriod = document.getElementById("adminDistribution-SyncPackageServer-period").innerHTML; + var syncPkgSvrDescription = document.getElementById("adminDistribution-SyncPackageServer-Description").innerHTML; + + if (syncPkgSvrUrl) { + syncPkgSvrUrl = syncPkgSvrUrl.replace("Package server url : ", ""); + } else { + syncPkgSvrUrl = "" + } + + if (syncPkgSvrPeriod) { + syncPkgSvrPeriod = syncPkgSvrPeriod.replace("Period : ", ""); + } else { + syncPkgSvrPeriod = "" + } + if (syncPkgSvrDescription) { + syncPkgSvrDescription = syncPkgSvrDescription.replace("Description : ", ""); + } else { + syncPkgSvrDescription = "" + } + + document.getElementById('adminDistributionModifyPopup-PackageServerUrl').value = packageServerUrl; + document.getElementById('adminDistributionModifyPopup-PackageServerAddress').value = packageServeraddress; + document.getElementById('adminDistributionModifyPopup-Description').value = serverDescription; + document.getElementById('adminDistributionModifyPopup-SyncPackageServer-Url').value = syncPkgSvrUrl; + document.getElementById('adminDistributionModifyPopup-SyncPackageServer-Period').value = syncPkgSvrPeriod; + document.getElementById('adminDistributionModifyPopup-SyncPackageServer-Description').value = syncPkgSvrDescription; + + $("#adminDistributionModifyPopup-Status").empty(); + var option; + if(serverStatusText.toUpperCase() == "OPEN") { + option = ''; + } else { + option = ''; + } + $("#adminDistributionModifyPopup-Status").append(option); + + if(serverStatusText.toUpperCase() == "CLOSE") { + option = ''; + } else { + option = ''; + } + $("#adminDistributionModifyPopup-Status").append(option); + $("#adminDistributionModifyPopup-Status").selectmenu("refresh"); +} + +function adminDistributionModify() { + var changeInfoList = []; + var changeInfoItem; + var distName = $("#adminDistributionSelect option:selected").val(); + var url = document.getElementById("adminDistributionModifyPopup-PackageServerUrl").value; + var address = document.getElementById("adminDistributionModifyPopup-PackageServerAddress").value; + var description = document.getElementById("adminDistributionModifyPopup-Description").value; + var distStatus = $("#adminDistributionModifyPopup-Status option:selected").val(); + var syncPkgSvrUrl = document.getElementById("adminDistributionModifyPopup-SyncPackageServer-Url").value; + var syncPkgSvrPeriod = document.getElementById("adminDistributionModifyPopup-SyncPackageServer-Period").value; + var syncPkgSvrDescription = document.getElementById("adminDistributionModifyPopup-SyncPackageServer-Description").value; + + if(distName == "" || url == "" || address == ""){ + alert("You must input full data"); + return; + } + + if(distStatus == "") { + distStatus = "OPEN"; + } + + changeInfoItem = {"DistributionName":distName, "URL":url, "Address":address, "DistStatus":distStatus, "Description":description, "SyncPkgSvrUrl":syncPkgSvrUrl, "SyncPkgSvrPeriod":syncPkgSvrPeriod, "SyncPkgSvrDescription":syncPkgSvrDescription}; + changeInfoList.push(changeInfoItem); + + modifyDistribution( changeInfoList, function () { + $.mobile.changePage("#adminDistribution"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-distribution.js b/dibs-web/public/javascripts/admin-distribution.js new file mode 100644 index 0000000..12e112b --- /dev/null +++ b/dibs-web/public/javascripts/admin-distribution.js @@ -0,0 +1,130 @@ +/* + admin-distribution.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminDistributionInit() { + queryAllDistribution( function (xml) { + var oldDistName = $("#adminDistributionSelect option:selected").val(); + var find = false; + var distributionList = $(xml).find("Data").find("DistributionName"); + + // remove old select options + $("#adminDistributionSelect").empty(); + + distributionList.each(function(){ + var name = $(this).text(); + + if( oldDistName == name ) { + $("#adminDistributionSelect").append(""); + find = true; + + } else { + $("#adminDistributionSelect").append(""); + } + }); + + /* default distribution selection */ + if(!find) { + $("#adminDistributionSelect option:eq(0)").attr("selected", "selected"); + } + + $("#adminDistributionSelect").selectmenu('refresh'); + + // set distribution info + adminDistributionSetInfo(); + }); +} + +function adminDistributionSetInfo() { + var distName = $("#adminDistributionSelect option:selected").val(); + + queryDistributionInfo( distName, function (xml) { + var data = $(xml).find("Data").find("DistributionInfo"); + var syncPackageServer = $(xml).find("Data").find("SyncPackageServer"); + var name = data.find("DistributionName").text(); + var url = data.find("PackageServerUrl").text(); + var address = data.find("PackageServerAddress").text(); + var distStatus = data.find("Status").text(); + var distDescription = data.find("Description").text(); + + $("#adminDistribution\\:packageServerUrl").text(url); + $("#adminDistribution\\:packageServerAddress").text(address); + $("#adminDistribution\\:distributionStatus").text(distStatus); + $("#adminDistribution\\:distributionDescription").text(distDescription); + + adminDistributionInitSyncPackageServer(syncPackageServer); + }); +} + +function adminDistributionRemove() { + var changeInfoList = []; + var changeInfoItem; + var distName = $("#adminDistributionSelect option:selected").val(); + + changeInfoItem = {"DistributionName":distName}; + changeInfoList.push(changeInfoItem); + + var r=confirm("Distribution ["+distName+"] will be removed!!!"); + if (r==false) + { + return; + } + + removeDistribution( changeInfoList, function (xml) { + $.mobile.changePage("#adminDistribution"); + }); +} + +function adminDistributionInitSyncPackageServer(serverInfo){ + $("#adminDistribution-SyncPackageServer").empty(); + + var info = '

Package server url : '+serverInfo.find("Url").text()+'

'; + info += '

Period : '+serverInfo.find("Period").text()+'

'; + info += '

Description : '+serverInfo.find("Description").text()+'

'; + + $("#adminDistribution-SyncPackageServer").append(info); +} + +function adminDistributionFullBuild() { + var changeInfoList = []; + var changeInfoItem; + var distName = $("#adminDistributionSelect option:selected").val(); + + changeInfoItem = {"DistributionName":distName}; + changeInfoList.push(changeInfoItem); + + var r=confirm("Distribution ["+distName+"] fullbuild will be started!!! it takes several time"); + if (r==false) + { + return; + } + + fullBuildDistribution( changeInfoList, function (xml) { + $.mobile.changePage("#adminDistribution"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-group-add.js b/dibs-web/public/javascripts/admin-group-add.js new file mode 100644 index 0000000..60c7a16 --- /dev/null +++ b/dibs-web/public/javascripts/admin-group-add.js @@ -0,0 +1,101 @@ +/* + admin-group-add.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminGroupAddInit() { + document.getElementById('adminGroupAddPopup-Name').value = ""; + document.getElementById('adminGroupAddPopup-Description').value = ""; + + queryAllProject( function(xml) { + var fullProjectList = $(xml).find("Data").find("Project"); + + adminGroupAddGenerateProjectSelect(fullProjectList); + }); +} + +function adminGroupAddGenerateProjectSelect(projectList) { + fieldset = document.getElementById('popup:addProjectCheckbox'); + + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project list"; + fieldset.appendChild(legend); + + projectList.each(function(){ + var projectName = $(this).find("Name").text(); + var projectId = $(this).find("Id").text(); + var projectDistName = $(this).find("DistName").text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:addGroupProjectCheckbox:'+projectId; + input.name = 'popup:addGroupProjectCheckbox'; + input.value = projectName; + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:addGroupProjectCheckbox:'+projectId); + label.innerHTML = projectName + "[" + projectDistName + "]"; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $("[name='popup\\:addGroupProjectCheckbox']").checkboxradio(); +} + +function adminGroupAddGroup() { + var selectArray = document.getElementsByName('popup:addGroupProjectCheckbox'); + var groupName = document.getElementById('adminGroupAddPopup-Name').value; + var adminFlag = $("#adminGroupAddPopup-Admin option:selected").val(); + var description = document.getElementById('adminGroupAddPopup-Description').value; + var selectProjectIdList = ""; + var changeInfoList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var projectId = selectArray[i].id.split(":")[2]; + selectProjectIdList = selectProjectIdList + "," + projectId; + } + } + + if(selectProjectIdList.length > 0) { + selectProjectIdList = selectProjectIdList.substring(1,selectProjectIdList.length); + } + + changeItem = {"GroupName" : groupName, "AdminFlag":adminFlag, "Description":description, "ProjectList" : selectProjectIdList}; + changeInfoList.push(changeItem); + + addGroup(changeInfoList, function() { + $.mobile.changePage("#adminGroup"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-group-modify.js b/dibs-web/public/javascripts/admin-group-modify.js new file mode 100644 index 0000000..17ed56e --- /dev/null +++ b/dibs-web/public/javascripts/admin-group-modify.js @@ -0,0 +1,129 @@ +/* + admin-group-modify.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminGroupModifyInit() { + var index = localStorage.groupTableIndex; + var groupName = document.getElementById("groupTableName:"+index).innerHTML; + var description = document.getElementById("groupTableDescription:"+index).innerHTML; + var adminFlagText = document.getElementById("groupTableAdminFlag:"+index).innerHTML; + + document.getElementById('adminGroupModifyPopup-Name').value = groupName; + document.getElementById('adminGroupModifyPopup-NewName').value = groupName; + document.getElementById('adminGroupModifyPopup-Description').value = description; + $("#adminGroupModifyPopup-Admin").empty(); + var option; + if(adminFlagText.toUpperCase() == "TRUE") { + option = ''; + } else { + option = ''; + } + $("#adminGroupModifyPopup-Admin").append(option); + + if(adminFlagText.toUpperCase() == "FALSE") { + option = ''; + } else { + option = ''; + } + $("#adminGroupModifyPopup-Admin").append(option); + $("#adminGroupModifyPopup-Admin").selectmenu("refresh"); + + queryGroupInfo(groupName, function(xml) { + var fullProjectList = $(xml).find("Data").find("Project"); + var projectIdList = $(xml).find("Data").find("Group").find("ProjectList").text().split(","); + + adminGroupModifyGenerateProjectSelect(fullProjectList, projectIdList); + }); +} + +function adminGroupModifyGenerateProjectSelect(fullProjectList, projectIdList) { + fieldset = document.getElementById('popup:modifyProjectSelect'); + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project list"; + fieldset.appendChild(legend); + + fullProjectList.each(function(){ + var projectName = $(this).find("Name").text(); + var projectId = $(this).find("Id").text(); + var projectDistName = $(this).find("DistName").text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:modifyProjectCheckbox:'+projectId; + input.name = 'popup:modifyProjectCheckbox'; + input.value = projectName; + + if(contains(projectIdList, projectId)) + { + input.setAttribute('checked', 'checked'); + } + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:modifyProjectCheckbox:'+projectId); + label.innerHTML = projectName + "[" + projectDistName + "]"; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $("[name='popup\\:modifyProjectCheckbox']").checkboxradio(); +} + +function adminGroupModifyGroup() { + var selectArray = document.getElementsByName('popup:modifyProjectCheckbox'); + var oldGroupName = document.getElementById('adminGroupModifyPopup-Name').value; + var groupName = document.getElementById('adminGroupModifyPopup-NewName').value; + var description = document.getElementById('adminGroupModifyPopup-Description').value; + var adminFlag = $("#adminGroupModifyPopup-Admin option:selected").val(); + var selectProjectIdList = ""; + var changeInfoList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var projectId = selectArray[i].id.split(":")[2]; + selectProjectIdList = selectProjectIdList + "," + projectId; + } + } + + if(selectProjectIdList.length > 0) { + selectProjectIdList = selectProjectIdList.substring(1,selectProjectIdList.length); + } + + changeItem = {"GroupName" : oldGroupName, "NewGroupName":groupName, "AdminFlag":adminFlag, "Description":description, "ProjectList" : selectProjectIdList}; + changeInfoList.push(changeItem); + + changeGroup(changeInfoList, function() { + $.mobile.changePage("#adminGroup"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-group.js b/dibs-web/public/javascripts/admin-group.js new file mode 100644 index 0000000..6f2104b --- /dev/null +++ b/dibs-web/public/javascripts/admin-group.js @@ -0,0 +1,165 @@ +/* + admin-group.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminGroupInit() { + adminGroupInitTable(); + + queryAllGroup( function(xml) { + var header = $(xml).find("Header"); + var groupList = $(xml).find("Data").find("Group"); + + adminGroupFillTableInfo(groupList); + }); +} + +function adminGroupInitTable() { + var groupTable = document.getElementById("groupTable"); + + /* remove all table rows */ + while(groupTable.hasChildNodes()) + { + groupTable.removeChild(groupTable.firstChild); + } +} + +function adminGroupFillTableInfo(groupList) { + var groupTable = document.getElementById("groupTable"); + + /* remove all table rows */ + while(groupTable.hasChildNodes()) + { + groupTable.removeChild(groupTable.firstChild); + } + + /* create table header */ + var row = groupTable.insertRow(-1); + var tableHeader = " Group name Project list Admin Description Modify Delete "; + + $("#groupTable").append(tableHeader); + + var index = 2; + + groupList.each(function(){ + var row = groupTable.insertRow(-1); + var cell; + var groupName = $(this).find("GroupName").text(); + var adminFlag = $(this).find("AdminFlag").text(); + var description = $(this).find("Description").text(); + var projectList = $(this).find("AccessableProject"); + + row.setAttribute('id', 'table:'+index); + + cell = row.insertCell(-1); + cell.setAttribute('id', 'groupTableName:'+index); + cell.innerHTML = groupName + + cell = row.insertCell(-1); + div = adminGroupGenerateProjectListCell(projectList); + cell.appendChild(div); + + cell = row.insertCell(-1); + cell.setAttribute('id', 'groupTableAdminFlag:'+index); + cell.innerHTML = adminFlag; + + cell = row.insertCell(-1); + cell.setAttribute('id', 'groupTableDescription:'+index); + cell.innerHTML = description; + + cell = row.insertCell(-1); + var button = document.createElement('a'); + button.setAttribute('href','#adminGroupModifyPopup'); + button.setAttribute('data-role','button'); + button.setAttribute('data-rel','dialog'); + button.setAttribute('onClick','adminGroupModifyPopupSetup(\"'+index+'\")'); + button.setAttribute('class','groupTableCellButton'); + button.innerHTML = " " + cell.appendChild(button); + + cell = row.insertCell(-1); + var button = document.createElement('input'); + button.setAttribute('type','button'); + button.setAttribute('onClick','adminGroupRemoveGroup('+index+')'); + button.setAttribute('class','groupTableCellButton'); + cell.appendChild(button); + + index = index + 1; + }); + + $(".groupProjectList").collapsible(); + $(".groupTableCellButton").button(); +} + +function adminGroupModifyPopupSetup(index) { + localStorage.groupTableIndex = index; +} + +function adminGroupGenerateProjectListCell(projectList) { + var div = document.createElement('div'); + div.setAttribute('data-role', 'collapsible'); + div.setAttribute('data-content-theme', 'b'); + div.setAttribute('class', 'groupProjectList'); + div.setAttribute('style', 'text-align: left'); + + var header = document.createElement('h3'); + header.innerHTML = "Project list"; + div.appendChild(header); + + var ul = document.createElement('ul'); + + projectList.each(function(){ + var projectName = $(this).find("ProjectName").text(); + var projectDistName = $(this).find("ProjectDistribution").text(); + var item = document.createElement('li'); + + item.innerHTML = projectName+"["+projectDistName+"]"; + ul.appendChild(item); + }); + + div.appendChild(ul); + return div; +} + +function adminGroupRemoveGroup(index) { + var groupName = document.getElementById("groupTableName:"+index).innerHTML; + + var r=confirm("User ["+groupName+"] will be removed!!!"); + if (r==false) + { + return; + } + + var changeInfoList = []; + var changeInfoItem; + changeInfoItem = {"GroupName":groupName}; + changeInfoList.push(changeInfoItem); + + removeGroup(changeInfoList, function() { + $.mobile.changePage("#adminGroup"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-project-add.js b/dibs-web/public/javascripts/admin-project-add.js new file mode 100644 index 0000000..7251408 --- /dev/null +++ b/dibs-web/public/javascripts/admin-project-add.js @@ -0,0 +1,177 @@ +/* + admin-project-add.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminProjectAddGitInit() { + document.getElementById("popup:addGitProjectName").value = ""; + document.getElementById("popup:addGitProjectPassword").value = ""; + document.getElementById("popup:addGitAddress").value = ""; + document.getElementById("popup:addGitBranch").value = ""; + + queryAllOS( function (xml) { + var osList = $(xml).find("Data").find("OsName"); + + fieldset = document.getElementById('popup:addGitProjectOs'); + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project os list"; + fieldset.appendChild(legend); + + osList.each(function(){ + var osName = $(this).text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:addGitProjectOsCheckbox:'+osName; + input.name = 'popup:addGitProjectOsCheckbox'; + input.value = osName; + input.setAttribute('class', 'popup:addGitProjectOsCheckbox'); + input.setAttribute('checked', 'checked'); + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:addGitProjectOsCheckbox:'+osName); + label.innerHTML = osName; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $('.popup\\:addGitProjectOsCheckbox').checkboxradio(); + }); +} + +function adminProjectAddBinaryInit() { + document.getElementById("popup:addBinaryProjectName").value = ""; + document.getElementById("popup:addBinaryProjectPassword").value = ""; + document.getElementById("popup:addBinaryPackageName").value = ""; + + queryAllOS( function (xml) { + var osList = $(xml).find("Data").find("OsName"); + + fieldset = document.getElementById('popup:addBinaryProjectOs'); + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project os list"; + fieldset.appendChild(legend); + + osList.each(function(){ + var osName = $(this).text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:addBinaryProjectOsCheckbox:'+osName; + input.name = 'popup:addBinaryProjectOsCheckbox'; + input.value = osName; + input.setAttribute('class', 'popup:addBinaryProjectOsCheckbox'); + input.setAttribute('checked', 'checked'); + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:addBinaryProjectOsCheckbox:'+osName); + label.innerHTML = osName; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $('.popup\\:addBinaryProjectOsCheckbox').checkboxradio(); + }); +} + +function adminProjectAddGitProject() { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + var changeInfoList = []; + var changeInfoItem; + var type = "GIT"; + var name = document.getElementById("popup:addGitProjectName").value; + var password = document.getElementById("popup:addGitProjectPassword").value; + var address = document.getElementById("popup:addGitAddress").value; + var branch = document.getElementById("popup:addGitBranch").value; + var selectArray = document.getElementsByName('popup:addGitProjectOsCheckbox'); + var selectOsList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var osName = selectArray[i].id.split(":")[2]; + selectOsList.push(osName); + } + } + + + if(name == "" || password == "" || address == "" || branch == ""){ + alert("You must input full data(Project name, Git password, Git address, Branch)"); + return; + } + + changeInfoItem = {"Distribution":distName, "Name":name, "ProjectType":type, "ProjectPass":password, "Address":address, "Branch":branch, "OSNameList":selectOsList.toString()}; + changeInfoList.push(changeInfoItem); + + addProject(changeInfoList, function () { + $.mobile.changePage("#adminProject"); + }); +} + +function adminProjectAddBinaryProject() { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + var changeInfoList = []; + var changeInfoItem; + var type = "BINARY"; + var name = document.getElementById("popup:addBinaryProjectName").value; + var password = document.getElementById("popup:addBinaryProjectPassword").value; + var pkgName = document.getElementById("popup:addBinaryPackageName").value; + var selectArray = document.getElementsByName('popup:addBinaryProjectOsCheckbox'); + var selectOsList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var osName = selectArray[i].id.split(":")[2]; + selectOsList.push(osName); + } + } + + if(name == "" || pkgName == ""){ + alert("You must input full data(Project name, Package name"); + return; + } + + changeInfoItem = {"Distribution":distName, "Name":name, "ProjectType":type, "ProjectPass":password, "PackageName":pkgName, "OSNameList":selectOsList.toString()}; + changeInfoList.push(changeInfoItem); + + addProject(changeInfoList, function () { + $.mobile.changePage("#adminProject"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-project-modify.js b/dibs-web/public/javascripts/admin-project-modify.js new file mode 100644 index 0000000..420d093 --- /dev/null +++ b/dibs-web/public/javascripts/admin-project-modify.js @@ -0,0 +1,209 @@ +/* + admin-project-modify.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminProjectModifyBinaryProjectInit() { + var projectName = localStorage.projectName; + var packageName = document.getElementById('modifyBinaryPackageName:'+projectName).innerHTML; + + document.getElementById('popup:modifyBinaryOldProjectName').value = projectName; + document.getElementById('popup:modifyBinaryNewProjectName').value = projectName; + document.getElementById('popup:modifyBinaryProjectPassword').value = ""; + document.getElementById('popup:modifyBinaryPackageName').value = packageName; + + queryAllOS( function (xml) { + var osList = $(xml).find("Data").find("OsName"); + var selectedOsList = []; + var projectName = localStorage.projectName; + + var osListElement = document.getElementById('adminBINARYProjectTableOsList:'+projectName); + for(var i = 0; i < osListElement.childNodes.length; i++) + { + selectedOsList.push(osListElement.childNodes[i].innerHTML); + } + + fieldset = document.getElementById('popup:modifyBinaryProjectOs'); + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project os list"; + fieldset.appendChild(legend); + + osList.each(function(){ + var osName = $(this).text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:modifyBinaryProjectOs:'+osName; + input.name = 'popup:modifyBinaryProjectOs' + input.value = osName; + input.setAttribute('class', 'popup:modifyBinaryProjectOs'); + if(contains(selectedOsList, osName)) + { + input.setAttribute('checked', 'checked'); + } + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:modifyBinaryProjectOs:'+osName); + label.innerHTML = osName; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $('.popup\\:modifyBinaryProjectOs').checkboxradio(); + }); +} + +function adminProjectModifyGitProjectInit() { + var projectName = localStorage.projectName; + var projectAddress = document.getElementById('modifyGitProjectAddress:'+projectName).innerHTML; + var projectBranch = document.getElementById('modifyGitProjectBranch:'+projectName).innerHTML; + + document.getElementById('popup:modifyGitOldProjectName').value = projectName; + document.getElementById('popup:modifyGitNewProjectName').value = projectName; + document.getElementById('popup:modifyGitProjectPassword').value = ""; + document.getElementById('popup:modifyGitProjectAddress').value = projectAddress; + document.getElementById('popup:modifyGitProjectBranch').value = projectBranch; + + queryAllOS( function (xml) { + var osList = $(xml).find("Data").find("OsName"); + var selectedOsList = []; + var projectName = localStorage.projectName; + + var osListElement = document.getElementById('adminGITProjectTableOsList:'+projectName); + for(var i = 0; i < osListElement.childNodes.length; i++) + { + selectedOsList.push(osListElement.childNodes[i].innerHTML); + } + + fieldset = document.getElementById('popup:modifyGitProjectOs'); + /* remove all table rows */ + while(fieldset.hasChildNodes()) + { + fieldset.removeChild(fieldset.firstChild); + } + + legend = document.createElement('legend'); + legend.innerHTML = "Project os list"; + fieldset.appendChild(legend); + + osList.each(function(){ + var osName = $(this).text(); + + var input = document.createElement('input'); + input.type = 'checkbox'; + input.id = 'popup:modifyGitProjectOs:'+osName; + input.name = 'popup:modifyGitProjectOs' + input.value = osName; + input.setAttribute('class', 'popup:modifyGitProjectOs'); + if(contains(selectedOsList, osName)) + { + input.setAttribute('checked', 'checked'); + } + + var label = document.createElement('label'); + label.setAttribute('for', 'popup:modifyGitProjectOs:'+osName); + label.innerHTML = osName; + + fieldset.appendChild(input); + fieldset.appendChild(label); + }); + + $('.popup\\:modifyGitProjectOs').checkboxradio(); + }); +} + +function adminProjectModfyBinaryProject() { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + var changeInfoList = []; + var changeInfoItem; + var oldProjectName = document.getElementById('popup:modifyBinaryOldProjectName').value; + var newProjectName = document.getElementById('popup:modifyBinaryNewProjectName').value; + var projectType = document.getElementById('popup:modifyBinaryProjectType').value; + var projectPassword = document.getElementById('popup:modifyBinaryProjectPassword').value; + var packageName = document.getElementById('popup:modifyBinaryPackageName').value; + var selectArray = document.getElementsByName('popup:modifyBinaryProjectOs'); + var selectOsList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var osName = selectArray[i].id.split(":")[2]; + selectOsList.push(osName); + } + } + + if(oldProjectName == "" || newProjectName == "" || projectPassword == "" || projectType == "" || packageName == ""){ + alert("You must input full data"); + return; + } + + changeInfoItem = {"Distribution":distName, "Name":oldProjectName, "NewProjectName":newProjectName, "ProjectType":projectType, "ProjectPass":projectPassword, "PackageName":packageName, "OSNameList":selectOsList.toString()}; + changeInfoList.push(changeInfoItem); + + modifyProject(changeInfoList, function () { + $.mobile.changePage("#adminProject"); + }); +} + +function adminProjectModfyGitProject() { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + var changeInfoList = []; + var changeInfoItem; + var oldProjectName = document.getElementById('popup:modifyGitOldProjectName').value; + var newProjectName = document.getElementById('popup:modifyGitNewProjectName').value; + var projectType = document.getElementById('popup:modifyGitProjectType').value; + var projectPassword = document.getElementById('popup:modifyGitProjectPassword').value; + var projectAddress = document.getElementById('popup:modifyGitProjectAddress').value; + var projectBranch = document.getElementById('popup:modifyGitProjectBranch').value; + var selectArray = document.getElementsByName('popup:modifyGitProjectOs'); + var selectOsList = []; + + for(var i = 0; i < selectArray.length; i++) { + if (selectArray[i].checked == true) { + var osName = selectArray[i].id.split(":")[2]; + selectOsList.push(osName); + } + } + + if(oldProjectName == "" || newProjectName == "" || projectPassword == "" || projectType == "" || projectAddress == "" || projectBranch == ""){ + alert("You must input full data"); + return; + } + + changeInfoItem = {"Distribution":distName, "Name":oldProjectName, "NewProjectName":newProjectName, "ProjectType":projectType, "ProjectPass":projectPassword, "ProjectAddress":projectAddress, "ProjectBranch":projectBranch, "OSNameList":selectOsList.toString()}; + changeInfoList.push(changeInfoItem); + + modifyProject(changeInfoList, function () { + $.mobile.changePage("#adminProject"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-project.js b/dibs-web/public/javascripts/admin-project.js new file mode 100644 index 0000000..03e06b7 --- /dev/null +++ b/dibs-web/public/javascripts/admin-project.js @@ -0,0 +1,255 @@ +/* + admin-project.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminProjectInit() { + queryAllDistribution( function (xml) { + var oldDistName = $("#adminProjectDistributionSelect option:selected").val(); + var find = false; + var distributionList = $(xml).find("Data").find("DistributionName"); + + // remove old select options + $("#adminProjectDistributionSelect").empty(); + + distributionList.each(function(){ + var name = $(this).text(); + + if( oldDistName == name ) { + $("#adminProjectDistributionSelect").append(""); + find = true; + + } else { + $("#adminProjectDistributionSelect").append(""); + } + }); + + /* default distribution selection */ + if(!find) { + $("#adminProjectDistributionSelect option:eq(0)").attr("selected", "selected"); + } + + $("#adminProjectDistributionSelect").selectmenu('refresh'); + + // set project info + adminProjectSetProjectInfo(); + }); +} + +function adminProjectSetProjectInfo() { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + + queryProjectsInDistributionForAdmin( distName, function (xml) { + var projectList = $(xml).find("Data").find("Project"); + + // update project info + adminProjectUpdateTable(projectList); + }); +} + +function popupModifyProject(projectName) { + localStorage.projectName = projectName; +} + +function adminProjectRemoveProject(projectType, projectName ) { + var distName = $("#adminProjectDistributionSelect option:selected").val(); + + var r=confirm("Project ["+projectName+"] is removed!!!"); + if (r==false) + { + return; + } + + var changeInfoList = []; + var changeInfoItem; + changeInfoItem = {"Distribution":distName, "ProjectType":projectType, "Name":projectName}; + changeInfoList.push(changeInfoItem); + + removeProject(changeInfoList, function () { + $.mobile.changePage("#adminProject"); + }); +} + +function adminProjectUpdateTable(projectList) { + /* project table */ + var projectTable = document.getElementById("adminProjectTable"); + + /* binary project table */ + var binaryProjectTable = document.getElementById("adminBinaryProjectTable"); + + /* remove all table rows */ + while(projectTable.hasChildNodes()) + { + projectTable.removeChild(projectTable.firstChild); + } + while(binaryProjectTable.hasChildNodes()) + { + binaryProjectTable.removeChild(binaryProjectTable.firstChild); + } + + // Project table header + var tableHeader = "ProjectGit reposBranchOS listModifyDelete"; + $("#adminProjectTable").append(tableHeader); + + // Binary project table header + var tableHeader = "ProjectPackage nameOS listModifyDelete"; + $("#adminBinaryProjectTable").append(tableHeader); + + var projectIdx = 1; + var binaryProjectIdx = 1; + // add project information + projectList.each(function(){ + var name = $(this).find("ProjectName").text(); + var type = $(this).find("Type").text(); + var osList = $(this).find("OS"); + + if(type.toUpperCase() == "GIT") + { + var row = projectTable.insertRow(-1); + var cell; + + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('id',"modifyGitProjectName:"+name); + cell.innerHTML = name; + + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('id',"modifyGitProjectAddress:"+name); + cell.innerHTML = $(this).find("GitRepos").text(); + + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('id',"modifyGitProjectBranch:"+name); + cell.innerHTML = $(this).find("GitBranch").text(); + + cell = row.insertCell(-1); + div = adminProjectApendOsCell(osList, name, "GIT"); + cell.appendChild(div); + + cell = row.insertCell(-1); + var button = document.createElement('a'); + button.setAttribute('href','#modifyGitProject'); + button.setAttribute('data-role','button'); + button.setAttribute('data-rel','dialog'); + button.setAttribute('data-mini','true'); + button.setAttribute('onClick','popupModifyProject(\"'+name+'\")'); + button.setAttribute('class','binaryProjectTableButton'); + button.innerHTML = " " + cell.appendChild(button); + + cell = row.insertCell(-1); + var button = document.createElement('input'); + button.setAttribute('type','button'); + button.setAttribute('id','button:git:'+projectIdx+':'+name); + button.setAttribute('data-mini','true'); + button.setAttribute('name',name); + button.setAttribute('class','binaryProjectTableButton'); + button.setAttribute('onClick','adminProjectRemoveProject(\"GIT\"'+',\"'+name+'\"'+')'); + cell.appendChild(button); + + projectIdx = projectIdx + 1; + } + else if (type.toUpperCase() == "BINARY") + { + var row = binaryProjectTable.insertRow(-1); + var cell; + + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('id',"modifyBinaryProjectName:"+name); + cell.innerHTML = name; + + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('id',"modifyBinaryPackageName:"+name); + cell.innerHTML = $(this).find("PackageName").text(); + + cell = row.insertCell(-1); + div = adminProjectApendOsCell(osList, name, "BINARY"); + cell.appendChild(div); + + cell = row.insertCell(-1); + var button = document.createElement('a'); + button.setAttribute('href','#modifyBinaryProject'); + button.setAttribute('data-role','button'); + button.setAttribute('data-inline','true'); + button.setAttribute('data-mini','true'); + button.setAttribute('data-rel','dialog'); + button.setAttribute('onClick','popupModifyProject(\"'+name+'\")'); + button.setAttribute('class','binaryProjectTableButton'); + cell.appendChild(button); + + cell = row.insertCell(-1); + var button = document.createElement('input'); + button.setAttribute('type','button'); + button.setAttribute('class','binaryProjectTableButton'); + button.setAttribute('data-mini','true'); + button.setAttribute('name',name); + button.setAttribute('onClick','adminProjectRemoveProject(\"BINARY\"'+',\"'+name+'\"'+')'); + cell.appendChild(button); + + binaryProjectIdx = binaryProjectIdx + 1; + } + else + alert("Unknown project type"); + + }); + + $(".binaryProjectTableButton").button(); + $(".groupProjectList").collapsible(); +} + +function adminProjectApendOsCell(osList, projectName, projectType) { + + var div = document.createElement('div'); + + div.setAttribute('data-role', 'collapsible'); + div.setAttribute('data-mini', 'true'); + div.setAttribute('data-content-theme', 'b'); + div.setAttribute('class', 'groupProjectList'); + + var header = document.createElement('h3'); + header.innerHTML = "OS list"; + div.appendChild(header); + + var ul = document.createElement('ul'); + ul.setAttribute('id', 'admin'+projectType+'ProjectTableOsList:'+projectName); + + // if osList does not exist then just return + if (osList != undefined) { + osList.each(function(){ + var item = document.createElement('li'); + item.innerHTML = $(this).text(); + ul.appendChild(item); + }); + } + + div.appendChild(ul); + return div; +} + + diff --git a/dibs-web/public/javascripts/admin-server-add.js b/dibs-web/public/javascripts/admin-server-add.js new file mode 100644 index 0000000..18a47d2 --- /dev/null +++ b/dibs-web/public/javascripts/admin-server-add.js @@ -0,0 +1,133 @@ +/* + admin-server-add.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminServerAddSupportedOsInit() { + // Remove select option + $('#adminServer-AddSupportedOs-OsCategory').empty(); + + queryAllOSCategory(function(xml){ + $(xml).find("Data").find("OsCategoryName").each(function(){ + var name = $(this).text(); + + $('#adminServer-AddSupportedOs-OsCategory').append(""); + + $('#adminServer-AddSupportedOs-OsCategory').selectmenu('refresh'); + }); + }); +} + +function adminServerAddRemoteBuildServer() { + var changeInfoList = []; + var changeInfoItem; + + var address = document.getElementById("adminServer-AddRemoteBuildServer-Address").value; + var description = document.getElementById("adminServer-AddRemoteBuildServer-Description").value; + + if(address == ""){ + alert("You must input server address"); + return; + } + + changeInfoItem = {"Address":address, "Description":description}; + changeInfoList.push(changeInfoItem); + + addRemoteBuildServer(changeInfoList, function () { + document.getElementById('adminServer-AddRemoteBuildServer-Address').value = ""; + document.getElementById('adminServer-AddRemoteBuildServer-Description').value = ""; + + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerAddSupportedOs() { + var changeInfoList = []; + var changeInfoItem; + + var osName = document.getElementById("adminServer-AddSupportedOs-OsName").value; + var osCategory = $("#adminServer-AddSupportedOs-OsCategory option:selected").val(); + + if(osName == ""){ + alert("You must input server address"); + return; + } + + changeInfoItem = {"Name":osName, "OsCategory":osCategory}; + changeInfoList.push(changeInfoItem); + + addSupportedOS(changeInfoList, function () { + document.getElementById('adminServer-AddSupportedOs-OsName').value = ""; + + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerAddOSCategory() { + var changeInfoList = []; + var changeInfoItem; + + var categoryName = document.getElementById("adminServer-AddOsCategory-Name").value; + + if(categoryName == ""){ + alert("You must input category name"); + return; + } + + changeInfoItem = {"Name":categoryName}; + changeInfoList.push(changeInfoItem); + + addOSCategory(changeInfoList, function () { + document.getElementById('adminServer-AddOsCategory-Name').value = ""; + + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerAddServerInfo() { + var property = document.getElementById('adminServer-addServerInfo-property').value; + var value = document.getElementById('adminServer-addServerInfo-value').value; + var changeInfoList = []; + var changeInfoItem; + + if(property == ""){ + alert("property is empty"); + return; + } + + if(value == ""){ + alert("value is empty"); + return; + } + + changeInfoItem = {"Property":property, "Value":value}; + changeInfoList.push(changeInfoItem); + + addServerInfo(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-server-modify.js b/dibs-web/public/javascripts/admin-server-modify.js new file mode 100644 index 0000000..28e9c97 --- /dev/null +++ b/dibs-web/public/javascripts/admin-server-modify.js @@ -0,0 +1,125 @@ +/* + admin-server-modify.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminServerModifyRemoteBuildServerInit() { + var serverAddress = localStorage.remoteBuildServerAddress; + var serverDescription = localStorage.remoteBuildServerDescription; + + document.getElementById('adminServer-modifyRemoteBuildServer-OldAddress').value = serverAddress; + document.getElementById('adminServer-modifyRemoteBuildServer-newAddress').value = serverAddress; + document.getElementById('adminServer-modifyRemoteBuildServer-description').value = serverDescription +} + +function adminServerModifyServerInfoInit(property, value) { + document.getElementById('adminServer-modifyServerInfo-property').value = property; + document.getElementById('adminServer-modifyServerInfo-value').value = value; +} + +function adminServerModifySupportedOSInit(osName, osCategory) { + // Remove select option + $('#adminServer-ModifySupportedOs-OsCategory').empty(); + + document.getElementById('adminServer-ModifySupportedOs-OldOsName').value = osName; + document.getElementById('adminServer-MoidfySupportedOs-OsName').value = osName; + + queryAllOSCategory( function(xml){ + $(xml).find("Data").find("OsCategoryName").each(function(){ + var name = $(this).text(); + var option = "" + + if(name == osCategory) { + option = '' + } else { + option = '' + } + + $('#adminServer-ModifySupportedOs-OsCategory').append(option); + $('#adminServer-ModifySupportedOs-OsCategory').selectmenu("refresh"); + }); + }); +} + +function adminServerModifyRemoteBuildServer() { + var changeInfoList = []; + var changeInfoItem; + var serverAddress = document.getElementById('adminServer-modifyRemoteBuildServer-OldAddress').value; + var newServerAddress = document.getElementById('adminServer-modifyRemoteBuildServer-newAddress').value; + var description = document.getElementById('adminServer-modifyRemoteBuildServer-description').value; + + if(serverAddress == ""){ + alert("server address is invalid"); + return; + } + + changeInfoItem = {"Address":serverAddress, "NewAddress":newServerAddress, "Description":description}; + changeInfoList.push(changeInfoItem); + + modifyRemoteBuildServer(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerModifySupportedOS() { + var changeInfoList = []; + var changeInfoItem; + var oldOsName = document.getElementById('adminServer-ModifySupportedOs-OldOsName').value; + var osName = document.getElementById('adminServer-MoidfySupportedOs-OsName').value; + var osCategory = $("#adminServer-ModifySupportedOs-OsCategory option:selected").val(); + + changeInfoItem = {"Name":oldOsName, "NewName":osName, "OsCategory":osCategory}; + changeInfoList.push(changeInfoItem); + + modifySupportedOS(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerModifyServerInfo() { + var property = document.getElementById('adminServer-modifyServerInfo-property').value; + var value = document.getElementById('adminServer-modifyServerInfo-value').value; + var changeInfoList = []; + var changeInfoItem; + + if(property == ""){ + alert("property is empty"); + return; + } + + if(value == ""){ + alert("value is empty"); + return; + } + + changeInfoItem = {"Property":property, "Value":value}; + changeInfoList.push(changeInfoItem); + + modifyServerInfo(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-server-remove.js b/dibs-web/public/javascripts/admin-server-remove.js new file mode 100644 index 0000000..db74a01 --- /dev/null +++ b/dibs-web/public/javascripts/admin-server-remove.js @@ -0,0 +1,116 @@ +/* + admin-server-remove.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminServerRemoveOSCategoryInit() { + // Remove select option + $('#adminServer-RemoveOSCategory-CategorySelect').empty(); + + queryAllOSCategory(function(xml){ + $(xml).find("Data").find("OsCategoryName").each(function(){ + var name = $(this).text(); + + $('#adminServer-RemoveOSCategory-CategorySelect').append(""); + + $('#adminServer-RemoveOSCategory-CategorySelect').selectmenu('refresh'); + }); + }); +} + +function adminServerRemoveOSCategory() { + var changeInfoList = []; + var changeInfoItem; + var osCategory = $("#adminServer-RemoveOSCategory-CategorySelect option:selected").val(); + + if(osCategory == ""){ + alert("Os category is invalid"); + return; + } + + changeInfoItem = {"Name":osCategory}; + changeInfoList.push(changeInfoItem); + + removeOSCategory(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerRemoveSupportedOS() { + var changeInfoList = []; + var changeInfoItem; + var oldOsName = document.getElementById('adminServer-ModifySupportedOs-OldOsName').value; + var osName = document.getElementById('adminServer-MoidfySupportedOs-OsName').value; + + if(oldOsName != osName ){ + alert("Remove command must be same to original os name"); + return; + } + + changeInfoItem = {"Name":osName}; + changeInfoList.push(changeInfoItem); + + removeSupportedOS(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerRemoveServerInfo() { + var property = document.getElementById('adminServer-modifyServerInfo-property').value; + var changeInfoList = []; + var changeInfoItem; + + if(property == ""){ + alert("property is empty"); + return; + } + + changeInfoItem = {"Property":property}; + changeInfoList.push(changeInfoItem); + + removeServerInfo(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + +function adminServerRemoveRemoteBuildServer() { + var changeInfoList = []; + var changeInfoItem; + var serverAddress = document.getElementById('adminServer-modifyRemoteBuildServer-OldAddress').value; + + if(serverAddress == ""){ + alert("server address is invalid"); + return; + } + + changeInfoItem = {"Address":serverAddress}; + changeInfoList.push(changeInfoItem); + + removeRemoteBuildServer(changeInfoList, function () { + $.mobile.changePage("#adminServer"); + }); +} + diff --git a/dibs-web/public/javascripts/admin-server.js b/dibs-web/public/javascripts/admin-server.js new file mode 100644 index 0000000..6aae3e9 --- /dev/null +++ b/dibs-web/public/javascripts/admin-server.js @@ -0,0 +1,109 @@ +/* + admin-server.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminServerInit() { + $('#adminServer-RemoteBuildServer').collapsible(); + + queryAllServer( function (xml) { + var serverConfig = $(xml).find("Data").find("ServerConfig"); + var buildServerList = $(xml).find("Data").find("RemoteBuildServer"); + var syncPackageServerList = $(xml).find("Data").find("SyncPackageServer"); + adminServerInitServerInfo(serverConfig); + adminServerRemoteInitRemoteBuildServer(buildServerList); + }); + + queryAllOS( function (xml) { + $("#adminServer-SupportedOS").empty(); + + $(xml).find("Data").find("OS").each(function(){ + var osName = $(this).find("OsName").text(); + var osCategory = $(this).find("OsCategory").text(); + + var li = '
  • '; + li += '

    OS category : '+osCategory+'

    '; + li += '
  • '; + + $("#adminServer-SupportedOS").append(li); + }); + + $("#adminServer-SupportedOS").listview('refresh'); + }); +} + +function adminServerInitServerInfo(serverConfig){ + $("#adminServer-ServerInfo").empty(); + + serverConfig.each(function(){ + var property = $(this).find("Property").text(); + var value = $(this).find("Value").text(); + + var li = '
  • '; + li += '
  • '; + $("#adminServer-ServerInfo").append(li); + }); + + $("#adminServer-ServerInfo").listview('refresh'); +} + +function adminServerRemoteInitRemoteBuildServer(serverList){ + $("#adminServer-RemoteBuildServer").empty(); + + serverList.each(function(){ + var address = $(this).find("Address").text(); + var description = $(this).find("Description").text(); + + var li = '
  • '; + li += '

    Description : '+$(this).find("Description").text()+'

    '; + li += '

    Runtime infomation

    '; + li += '

    Status: '+$(this).find("Status").text()+'

    '; + li += '

    Supported OS: '+$(this).find("SupportedOS").text()+'

    '; + li += '

    Max job count: '+$(this).find("MaxJobCount").text()+'

    '; + li += '

    Working job count: '+$(this).find("WorkingJobCount").text()+'

    '; + li += '

    Waiting job count: '+$(this).find("WaitingJobCount").text()+'

    '; + li += '
  • '; + + $("#adminServer-RemoteBuildServer").append(li); + }); + + $("#adminServer-RemoteBuildServer").listview('refresh'); +} + +function popupModifyRemoteBuildServerInfo(address, description) { + localStorage.remoteBuildServerAddress = address; + localStorage.remoteBuildServerDescription = description; +} + diff --git a/dibs-web/public/javascripts/admin-user-modify.js b/dibs-web/public/javascripts/admin-user-modify.js new file mode 100644 index 0000000..fd00e41 --- /dev/null +++ b/dibs-web/public/javascripts/admin-user-modify.js @@ -0,0 +1,71 @@ +/* + admin-user-modify.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminUserModifyPopupInit() { + var index = localStorage.userTableIndex; + var userName = document.getElementById("userTableName:"+index).innerHTML; + var email = document.getElementById("userTableEmail:"+index).innerHTML; + var group = document.getElementById("userTableGroup:"+index).innerHTML; + + document.getElementById('popup:modifyUserName').value = userName; + mailElement = document.getElementById('popup:modifyUserEmail'); + mailElement.value = email; + mailElement.setAttribute('disabled', 'disabled'); + + queryAllGroup( function(xml) { + var groupList = $(xml).find("Data").find("GroupName"); + + $("#popup\\:modifyUserGroup").empty(); + + groupList.each( function(){ + var groupName = $(this).text(); + if( groupName == group ) { + $("#popup\\:modifyUserGroup").append(""); + } else { + $("#popup\\:modifyUserGroup").append(""); + } + + $("#popup\\:modifyUserGroup").selectmenu('refresh'); + }); + }); +} + +function adminUserModifyPopupModify() { + var userName = document.getElementById("popup:modifyUserName").value; + var email = document.getElementById("popup:modifyUserEmail").value; + var groupName = $("#popup\\:modifyUserGroup option:selected").val(); + + var changeInfoList = []; + var changeInfoItem; + changeInfoItem = {"Type":"ModifyUser", "Email":email, "UserName":userName, "GroupName":groupName}; + changeInfoList.push(changeInfoItem); + + changeUser(changeInfoList, function () { + }); +} + diff --git a/dibs-web/public/javascripts/admin-user.js b/dibs-web/public/javascripts/admin-user.js new file mode 100644 index 0000000..73acf00 --- /dev/null +++ b/dibs-web/public/javascripts/admin-user.js @@ -0,0 +1,119 @@ +/* + admin-user.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function adminUserInit() { + var userTable = document.getElementById("adminUserTable"); + + /* remove all table rows */ + while(userTable.hasChildNodes()) + { + userTable.removeChild(userTable.firstChild); + } + + /* create table header */ + var tableHeader = "User nameEmailGroupModifyDelete"; + $("#adminUserTable").append(tableHeader); + + queryAllUser( function(xml) { + var userList = $(xml).find("Data").find("User"); + + adminUserFillTable(userList); + }); +} + +function adminUserFillTable(userList) { + var userTable = document.getElementById("adminUserTable"); + var index = 2; + + userList.each(function(idx){ + var row = userTable.insertRow(-1); + var cell; + var userName = $(this).find("Name").text(); + var groupName = $(this).find("GroupName").text(); + var email = $(this).find("Email").text(); + + row.setAttribute('id', 'userTable:'+index); + cell = row.insertCell(-1); + cell.setAttribute('id',"userTableName:"+index); + cell.innerHTML = userName; + + cell = row.insertCell(-1); + cell.setAttribute('id',"userTableEmail:"+index); + cell.innerHTML = email; + + cell = row.insertCell(-1); + cell.setAttribute('id',"userTableGroup:"+index); + cell.innerHTML = groupName; + + cell = row.insertCell(-1); + var button = document.createElement('a'); + button.setAttribute('href','#adminUserModifyPopup'); + button.setAttribute('data-role','button'); + button.setAttribute('data-rel','dialog'); + button.setAttribute('class','adminUserTableButton'); + button.setAttribute('onClick','adminUserModifyPopupSetup(\"'+index+'\")'); + button.innerHTML = " " + cell.appendChild(button); + + cell = row.insertCell(-1); + var button = document.createElement('input'); + button.setAttribute('type','button'); + button.setAttribute('name',email); + button.setAttribute('class','adminUserTableButton'); + button.setAttribute('onClick','adminUserRemoveUser('+index+')'); + cell.appendChild(button); + + index = index + 1; + }); + + $(".adminUserTableButton").button(); +} + +function adminUserModifyPopupSetup(index) { + localStorage.userTableIndex = index; +} + +function adminUserRemoveUser(index) { + var email = document.getElementById("userTableEmail:"+index).innerHTML; + + var r=confirm("User ["+email+"] is removed!!!"); + if (r==false) + { + return; + } + + var changeInfoList = []; + var changeInfoItem; + changeInfoItem = {"Type":"RemoveUser", "Email":email}; + changeInfoList.push(changeInfoItem); + + removeUser(changeInfoList, function () { + $.mobile.changePage("#adminUser"); + }); +} + diff --git a/dibs-web/public/javascripts/application.js b/dibs-web/public/javascripts/application.js new file mode 100644 index 0000000..9097d83 --- /dev/null +++ b/dibs-web/public/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +// GO AFTER THE REQUIRES BELOW. +// +//= require jquery +//= require jquery_ujs +//= require_tree . diff --git a/dibs-web/public/javascripts/build.js b/dibs-web/public/javascripts/build.js new file mode 100644 index 0000000..c2efd25 --- /dev/null +++ b/dibs-web/public/javascripts/build.js @@ -0,0 +1,344 @@ +/* + build.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function buildInit() { + if( $("#select-distribution").children().length == 0 ) { + queryDistribution( function(xml) { + $(xml).find("Data").find("DistributionName").each(function(){ + var name = $(this).text(); + + $("#select-distribution").append(""); + }); + + /* default distribution selection */ + $("#select-distribution option:eq(0)").attr("selected", "selected"); + $("#select-distribution").selectmenu('refresh'); + + // query Project list + buildQueryProjectList(); + + // query Running project list + //buildQueryRunningProjectList(); + + // add batch file selector event + //buildBatchFilePathSelector(); + }); + } else { + // query Project list + buildQueryProjectList(); + + // query Running project list + //buildQueryRunningProjectList(); + + // add batch file selector event + //buildBatchFilePathSelector(); + } +} + +function buildQueryProjectList() { + var distName = $("#select-distribution option:selected").val(); + + queryProjectsInDistribution( distName, function(xml) { + buildInitTable(); + var xmlBody = $(xml).find("Data"); + buildAddTableRow( xmlBody.find("BuildServerInfo").find("supportedOs"), + xmlBody.find("Project"), + xmlBody.find("OtherProject")); + buildAddBinaryTableRow( xmlBody.find("BinaryProject"), + xmlBody.find("OtherBinaryProject")); + }); +} + +function buildQueryRunningProjectList() { + var distName = $("#select-distribution option:selected").val(); + + queryRunningProjectsInfoInDistribution( distName, function(xml) { + var running_project = $(xml).find("RunProjectInfo"); + running_project.each(function(){ + var project_name = $(this).find("ProjectName").text(); + var project_os = $(this).find("ProjectOs").text(); + var project_type = $(this).find("ProjectType").text(); + var project_status = $(this).find("Status").text(); + + if (project_type == "GIT") { + var cell = document.getElementById("buildGitProjectTable"+":"+project_name+':'+project_os); + + if(cell){ + cell.setAttribute('bgcolor', '#fbff00'); + } + } + }); + }); +} + +function buildSelectAll(element) { + var checkboxes = document.getElementsByName(element.id); + var checked = element.checked; + for(var i in checkboxes) { + if(!checkboxes[i].disabled) { + checkboxes[i].checked = checked; + } + } +} + +function contains(a, obj) { + for (var i = 0; i < a.length; i++) { + if (a[i] == obj) { + return true; + } + } + return false; +} + +function buildInitTable() { + /* project table */ + var projectTable = document.getElementById("projectTable"); + + /* binary project table */ + var binaryProjectTable = document.getElementById("binaryProjectTable"); + + /* remove all table rows */ + while(projectTable.hasChildNodes()) + { + projectTable.removeChild(projectTable.firstChild); + } + while(binaryProjectTable.hasChildNodes()) + { + binaryProjectTable.removeChild(binaryProjectTable.firstChild); + } +} + +function buildAddTableRow(supportedOs, projectList, otherProjectList) { + // Table header + var idx = 0; + var tableHeader = ""; + var osArray = new Array(); + var projectTable = document.getElementById("projectTable"); + + // Project table header + tableHeader = "Project"; + supportedOs.each(function(){ + var osName = $(this).text(); + tableHeader = tableHeader + ""+osName+""; + + osArray[idx] = osName; + idx++; + }); + + tableHeader = tableHeader + "ALL"; + $("#projectTable").append(tableHeader); + + // table row - projectList + var index = 2; + + projectList.each(function(){ + var name = $(this).find("ProjectName").text(); + var osLists = $(this).find("OsList").text(); + var osList = osLists.split(","); + + var row = projectTable.insertRow(-1); + + var cell = row.insertCell(0); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('bgcolor', '#dcddc0'); + cell.innerHTML = name; + + for (i=0;i'; + row += '' + packageName + '' + row += 'REGISTER'; + row += '' + + $("#binaryProjectTable tr:last").after(row); + }); + + otherProjectList.each(function(){ + var name = $(this).find("ProjectName").text(); + var packageName = $(this).find("PackageName").text(); + var row = binaryProjectTable.insertRow(-1); + + /* add project name */ + var cell = row.insertCell(0); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('bgcolor', '#c0c0c0'); + cell.innerHTML = name; + + /* add package name */ + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('bgcolor', '#c0c0c0'); + cell.innerHTML = packageName; + + /* add empty cell for register */ + cell = row.insertCell(-1); + cell.setAttribute('style', 'text-align: left'); + cell.setAttribute('bgcolor', '#c0c0c0'); + cell.innerHTML = ""; + }); + + $('.binary_project_button').button(); + $('.binary_project_button').popupWindow({ + height:200, + width:400, + top:50, + left:50 + }); +} + +function buildBuildProject(type) { + var distName = $("#select-distribution option:selected").val(); + var buildProjectList = []; + + //var node_list = document.getElementsByTagName('input'); + var node_list = $("#build input") + for (var i = 0; i < node_list.length; i++) { + var node = node_list[i]; + + if (node.getAttribute('type') == "checkbox") { + if (node.checked == true) { + if (node.getAttribute('name') != "all-checkbox") { + var prjName = node.id.split(":")[1]; + var osName = node.id.split(":")[2]; + var buildData = { "distribution":distName, "buildType":type, "projectName" : prjName, "os" : osName }; + buildProjectList.push(buildData); + } + } + } + } + + buildProject(buildProjectList, function () { + $.mobile.changePage("#jobs"); + }); +} + +function buildUploadBinaryName(project_name) { + localStorage.distibutionName = $("#select-distribution option:selected").val(); + localStorage.uploadBinaryProjectName = project_name; +} + +function buildBatchFilePathSelector() { + var files, file, extension; + var filePath = document.getElementById("buildBatchFileUploadPath"); + var filePathOutputList = document.getElementById("buildBatchFileList"); + + filePath.addEventListener("change", function(e) { + files = e.target.files; + filePathOutputList.innerHTML = ""; + + for (var i = 0, len = files.length; i < len; i++) { + file = files[i]; + alert(file.webkitRelativePath); + filePathOutputList.innerHTML += "
  • " + file.name + "
  • "; + } + }, false); +} + +function buildClear() { + $("#select-distribution").empty(); + + buildInitTable(); +} diff --git a/dibs-web/public/javascripts/dibs-api.js b/dibs-web/public/javascripts/dibs-api.js new file mode 100644 index 0000000..9802a71 --- /dev/null +++ b/dibs-web/public/javascripts/dibs-api.js @@ -0,0 +1,392 @@ +/* + dibs-api.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +// controller: Users +function signUp(infoList, successFunction) { + var url = 'users/signup'; + postForServer(url, infoList, successFunction); +} + +function queryUserInfo(successFunction) { + var url = 'users/query'; + getInfoFromServer(url, successFunction); +} + +function modifyUserInfo(changeInfoList, successFunction) { + var url = 'users/modify'; + postForServer(url, changeInfoList, successFunction); +} + +// controller: Sessions +function login(infoList, successFunction) { + var url = 'sessions/login'; + postForServer(url, infoList, successFunction); +} + +function logout() { + var url = 'sessions/logout'; + deleteForServer(url, function() { + expireSession(); + dibsWebClear(); + }); +} + +// controller: projects +function queryDistribution(successFunction) { + var url = 'projects/queryDistribution'; + getInfoFromServer(url, successFunction); +} + +// controller : projects +function buildProject(changeInfoList, successFunction) { + var url = 'projects/buildProject'; + postForServer(url, changeInfoList, successFunction); +} + +function queryProjectsInDistribution(distName, successFunction) { + var url = 'projects/queryProjectsInDistribution/' + distName; + getInfoFromServer(url, successFunction); +} + +function queryRunningProjectsInfoInDistribution(distName, successFunction) { + var url = 'projects/queryRunningProjectsInfoInDistribution/' + distName; + getInfoFromServer(url, successFunction); +} + +function queryProjectsInfoInDistribution(distName, successFunction) { + var url = 'projects/queryProjectsInfoInDistribution/' + distName; + getInfoFromServer(url, successFunction); +} + +// controller : jobs +function queryJobsList(url, successFunction) { + return getInfoFromServer(url, successFunction); +} +function updateList(data, successFunction) { + var url = "jobs/update"; + var param = $.param(data); + return getInfoFromServerData(url, param, successFunction); +} +function queryJobsLog(job_id, next_line, successFunction) { + var url = "jobs/log/"+ job_id + "/" + next_line; + getInfoFromServerNoPreProcess(url, successFunction); +} +function cancelJobsJobid(job_id, successFunction) { + var url = "jobs/cancel/"+ job_id; + getInfoFromServerNoPreProcess(url, successFunction); +} + +// controller : admin_group +function queryAllGroup(successFunction) { + var url = 'admin_group/queryAllGroup'; + getInfoFromServer(url, successFunction); +} + +function queryGroupInfo(groupName, successFunction) { + var url = 'admin_group/queryGroupInfo/' + groupName; + getInfoFromServer(url, successFunction); +} + +function addGroup(changeInfoList, successFunction) { + var url = 'admin_group/addGroup'; + postForServer(url, changeInfoList, successFunction); +} + +function removeGroup(changeInfoList, successFunction) { + var url = 'admin_group/removeGroup'; + postForServer(url, changeInfoList, successFunction); +} + +function changeGroup(changeInfoList, successFunction) { + var url = 'admin_group/modifyGroup'; + postForServer(url, changeInfoList, successFunction); +} + +// controller : admin_user +function queryAllUser(successFunction) { + var url = 'admin_user/queryAllUser'; + getInfoFromServer(url, successFunction); +} + +function changeUser(changeInfoList, successFunction) { + var url = 'admin_user/modifyUser'; + postForServer(url, changeInfoList, successFunction); +} + +function removeUser(changeInfoList, successFunction) { + var url = 'admin_user/removeUser'; + postForServer(url, changeInfoList, successFunction); +} + +// controller : admin_server +function queryServerInfo(successFunction) { + var url = 'admin_server/queryServerInfo'; + getInfoFromServer(url, successFunction); +} + +function queryAllServer(successFunction) { + var url = 'admin_server/queryAllServer'; + getInfoFromServer(url, successFunction); +} + +function addRemoteBuildServer(changeInfoList, successFunction) { + var url = 'admin_server/addRemoteBuildServer'; + postForServer(url, changeInfoList, successFunction); +} + +function removeRemoteBuildServer(changeInfoList, successFunction) { + var url = 'admin_server/removeRemoteBuildServer'; + postForServer(url, changeInfoList, successFunction); +} + +function modifyRemoteBuildServer(changeInfoList, successFunction) { + var url = 'admin_server/modifyRemoteBuildServer'; + postForServer(url, changeInfoList, successFunction); +} + +function addOSCategory(changeInfoList, successFunction) { + var url = 'admin_server/addOsCategory'; + postForServer(url, changeInfoList, successFunction); +} + +function removeOSCategory(changeInfoList, successFunction) { + var url = 'admin_server/removeOsCategory'; + postForServer(url, changeInfoList, successFunction); +} + +function modifyOSCategory(changeInfoList, successFunction) { + var url = 'admin_server/modifyOsCategory'; + postForServer(url, changeInfoList, successFunction); +} + +function addSupportedOS(changeInfoList, successFunction) { + var url = 'admin_server/addSupportedOS'; + postForServer(url, changeInfoList, successFunction); +} + +function removeSupportedOS(changeInfoList, successFunction) { + var url = 'admin_server/removeSupportedOS'; + postForServer(url, changeInfoList, successFunction); +} + +function modifySupportedOS(changeInfoList, successFunction) { + var url = 'admin_server/modifySupportedOS'; + postForServer(url, changeInfoList, successFunction); +} + +function addServerInfo(changeInfoList, successFunction) { + var url = 'admin_server/addServerInfo'; + postForServer(url, changeInfoList, successFunction); +} + +function removeServerInfo(changeInfoList, successFunction) { + var url = 'admin_server/removeServerInfo'; + postForServer(url, changeInfoList, successFunction); +} + +function modifyServerInfo(changeInfoList, successFunction) { + var url = 'admin_server/modifyServerInfo'; + postForServer(url, changeInfoList, successFunction); +} + +// controller : admin_project +function queryAllProject(successFunction) { + var url = 'admin_project/queryAllProject'; + getInfoFromServer(url, successFunction); +} + +function queryProjectsInDistributionForAdmin(distName, successFunction) { + var url = 'admin_project/queryProjectsInDistributionForAdmin/' + distName; + getInfoFromServer(url, successFunction); +} + +function addProject(changeInfoList, successFunction) { + var url = 'admin_project/addProject'; + postForServer(url, changeInfoList, successFunction); +} + +function removeProject(changeInfoList, successFunction) { + var url = 'admin_project/removeProject'; + postForServer(url, changeInfoList, successFunction); +} + +function modifyProject(changeInfoList, successFunction) { + var url = 'admin_project/modifyProject'; + postForServer(url, changeInfoList, successFunction); +} + +// controller : admin_distribution +function queryAllDistribution(successFunction) { + var url = 'admin_distribution/queryAllDistribution'; + getInfoFromServer(url, successFunction); +} + +function queryDistributionInfo(distName, successFunction) { + var url = 'admin_distribution/queryDistributionInfo/' + distName; + getInfoFromServer(url, successFunction); +} + +function addDistribution(changeInfoList, successFunction) { + var url = 'admin_distribution/addDistribution'; + postForServer(url, changeInfoList, successFunction); +} + +function removeDistribution(changeInfoList, successFunction) { + var url = 'admin_distribution/removeDistribution'; + postForServer(url, changeInfoList, successFunction); +} + +function modifyDistribution(changeInfoList, successFunction) { + var url = 'admin_distribution/modifyDistribution'; + postForServer(url, changeInfoList, successFunction); +} + +function fullBuildDistribution(changeInfoList, successFunction) { + var url = 'admin_distribution/fullBuildDistribution'; + postForServer(url, changeInfoList, successFunction); +} + +// controller : admin +function queryAllOS(successFunction) { + var url = 'admin/queryAllOS'; + getInfoFromServer(url, successFunction); +} + +function queryAllOSCategory(successFunction) { + var url = 'admin/queryAllOSCategory'; + getInfoFromServer(url, successFunction); +} + +function getInfoFromServer(url, successFunction) { + return $.ajax({ + url: baseUrl+url, + type: 'GET', + dataType: 'xml', + timeout: 10000, + beforeSend: function() { $.mobile.showPageLoadingMsg(); }, //Show spinner + complete: function() { $.mobile.hidePageLoadingMsg() }, //Hide spinner + success: function(xml) { + setSessionInfo(xml); + successFunction(xml); + }, + error: function(jqXHR) { + errorProcess(jqXHR); + } + }); +} + +function getInfoFromServerNoPreProcess(url, successFunction) { + return $.ajax({ + url: baseUrl+url, + type: 'GET', + dataType: 'xml', + timeout: 10000, + success: function(xml) { + setSessionInfo(xml); + successFunction(xml); + }, + error: function(jqXHR) { + errorProcess(jqXHR); + } + }); +} + +function getInfoFromServerData(url, data, successFunction) { + return $.ajax({ + url: baseUrl+url, + type: 'GET', + dataType: 'xml', + text: 'JSONP', + data: data, + timeout: 10000, + success: function(xml) { + setSessionInfo(xml); + successFunction(xml); + }, + error: function(jqXHR) { + errorProcess(jqXHR); + } + }); +} + +function postForServer(url, changeInfoList, successFunction) { + $.ajax({ + url: baseUrl+url, + type: 'POST', + data: JSON.stringify({ ChangeInfoList: changeInfoList }), + dataType: 'json', + contentType: "application/json; charset=utf-8", + timeout: 10000, + beforeSend: function() { $.mobile.showPageLoadingMsg(); }, //Show spinner + complete: function() { $.mobile.hidePageLoadingMsg() }, //Hide spinner + success: function(xml) { + successFunction(xml); + }, + error: function(jqXHR) { + errorProcess(jqXHR); + } + }); +} + +function deleteForServer(url, successFunction) { + $.ajax({ + url: baseUrl+url, + type: 'DELETE', + async: false, + cache: false, + dataType: 'xml', + timeout: 1000, + beforeSend: function() { $.mobile.showPageLoadingMsg(); }, //Show spinner + complete: function() { $.mobile.hidePageLoadingMsg() }, //Hide spinner + success: function(xml) { + expireSession(); + dibsWebClear(); + }, + error: function(jqXHR) { + errorProcess(jqXHR); + } + }); +} + +function errorProcess(jqXHR){ + switch (parseInt(jqXHR.status)) { + case 401: + expireSession(); + break; + case 406: + alert("Internal server error : " + jqXHR.responseText); + break; + case 0: + console.error("Error response status : "+jqXHR.status); + break; + default: + console.error("Error response status : "+jqXHR.status); + alert("Server error : "+jqXHR.status); + break; + } +} diff --git a/dibs-web/public/javascripts/jobs.js b/dibs-web/public/javascripts/jobs.js new file mode 100644 index 0000000..cde6c98 --- /dev/null +++ b/dibs-web/public/javascripts/jobs.js @@ -0,0 +1,695 @@ +/* + jobs.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +var suggestion_list = new Array(); +var update_ajax; +var request_list = new Array(); +var request_idx = 0; + +$(function() { + $('#jobSearchSelect input[type="radio"]').checkboxradio().click(function() { + jobsSearchSelected(); + }); + + $('#jobStatusSelect input[type="radio"]').checkboxradio().click(function() { + jobsStatusSelected(); + }); + + $("#jobSearchInputText").on("input", function(e) { + var selectedValue = $('#jobSearchSelect').find("input[type='radio']:checked").val(); + switch(selectedValue) { + case "GROUP": + case "PROJECT": + suggestJobSearchList($(this).val()); + break; + default: + return; + } + }); + + $('#jobSearchInputText').keypress(function() { + if(event.keyCode == '13') { + searchJobInput(); + } + }); + + $('#jobSearchDate').keypress(function() { + if(event.keyCode == '13') { + searchJobDate(); + } + }); +}); + +function jobsInit() { + jobQueryDistribution(); +} + +function jobsSearchSelected() { + clearSuggestJobSearchList(); + + var selectedValue = $('#jobSearchSelect').find("input[type='radio']:checked").val(); + switch(selectedValue) { + case "ALL": + selectJobAll(); + break; + case "JOBID": + selectJobId(); + break; + case "USER": + selectUser(); + break; + case "GROUP": + selectGroup(); + break; + case "PROJECT": + selectProject(); + break; + case "DATE": + selectDate(); + break; + default: + alert("Sorry. Please, report bug."); + break; + } +} + +function jobsStatusSelected() { + var selectedValue = $('#jobStatusSelect').find("input[type='radio']:checked").val(); + switch(selectedValue) { + case "ALL": + jobsSearchSelected(); + break; + case "SUCCESS": + jobsSearchSelected(); + break; + case "WORKING": + jobsSearchSelected(); + break; + case "ERROR": + jobsSearchSelected(); + break; + default: + alert("Sorry. Please, report bug."); + break; + } +} + +function searchJobInput() { + var searchText = $("#jobSearchInputText").val() + if(searchText.length > 0) { + searchJob(searchText); + } +} + +function searchJobDate() { + var searchText = $("#jobSearchDate").val() + if(searchText.length > 0) { + searchJob(searchText); + } +} + +function searchJob(searchText) { + var distribution = $("#jobSelectDistribution option:selected").val(); + var selectedValue = $('#jobSearchSelect').find("input[type='radio']:checked").val(); + switch(selectedValue) { + case "ALL": + alert("Can't search on ALL!!"); + break; + case "JOBID": + queryJobListJobId(distribution, searchText); + break; + case "USER": + queryJobListUserName(distribution, searchText); + break; + case "GROUP": + queryJobListUserGroup(distribution, searchText); + break; + case "PROJECT": + queryJobListProject(distribution, searchText); + break; + case "DATE": + queryJobListDate(distribution, searchText); + break; + default: + alert("Sorry. Please, report bug."); + break; + } +} + +function selectJobAll() { + var distribution = $("#jobSelectDistribution option:selected").val(); + $("#jobDivSearchDate").hide(); + $("#jobDivSearchInput").hide(); + queryJobListAll(distribution); +} + +function selectJobId() { + $("#jobDivSearchDate").hide(); + $("#jobDivSearchInput").show(); + $("#jobSearchInputText").val("").textinput(); + clearJobList(); +} + +function selectUser() { + var distribution = $("#jobSelectDistribution option:selected").val(); + $("#jobDivSearchDate").hide(); + $("#jobDivSearchInput").show(); + + var name = sessionStorage.sessionInfoName; + $("#jobSearchInputText").val(name).textinput(); + + queryJobListUserName(distribution, name); +} + +function selectGroup() { + var distribution = $("#jobSelectDistribution option:selected").val(); + $("#jobDivSearchDate").hide(); + $("#jobDivSearchInput").show(); + + var group = sessionStorage.sessionInfoGroup; + $("#jobSearchInputText").val(group).textinput(); + + jobsQueryGroupList("JOBS"); + + queryJobListUserGroup(distribution, group); +} + +function selectProject() { + var distribution = $("#jobSelectDistribution option:selected").val(); + $("#jobDivSearchDate").hide(); + $("#jobDivSearchInput").show(); + clearJobList(); + + $("#jobSearchInputText").val("").textinput(); + + jobsQueryProjectsList(distribution); +} + +function selectDate() { + var distribution = $("#jobSelectDistribution option:selected").val(); + var today = new Date(); + var yyyy = today.getFullYear(); + var mm = today.getMonth()+1; //January is 0! + var dd = today.getDate(); //January is 0! + if(dd<10) { + dd='0'+dd; + } + if(mm<10) { + mm='0'+mm; + } + + var date = yyyy+'-'+mm+'-'+dd; + $('#jobSearchDate').val(date); + + $("#jobDivSearchInput").hide(); + $("#jobDivSearchDate").show(); + clearJobList(); + queryJobListDate(distribution, date); +} + +function clearJobList() { + $("#jobList").empty(); +} + +function queryJobListAll(distribution) { + queryJobListByButton("all", "", distribution, "LATEST"); +} + +function queryJobListJobId(distribution, jobId) { + queryJobListByButton("all", "", distribution, eval(parseInt(jobId) + 1)); +} +function queryJobListUserName(distribution, name) { + var encodingName = encodeURIComponent(name); + queryJobListByButton("user", encodingName, distribution, "LATEST"); +} + +function queryJobListUserGroup(distribution, group) { + queryJobListByButton("group", group, distribution, "LATEST"); +} + +function queryJobListProject(distribution, project) { + queryJobListByButton("project", project, distribution, "LATEST"); +} + +function queryJobListDate(distribution, date) { + queryJobListByButton("date", date, distribution, "LATEST"); +} + +function queryJobListByButton(condition, param, distribution, jobId) { + clearJobList(); + clearRequestList(); + var selectedStatus= $('#jobStatusSelect').find("input[type='radio']:checked").val(); + var request = queryJobList("jobs/list", condition, param, distribution, selectedStatus, jobId); + if(request != undefined) { + request.done(function() { + console.log("Start update"); + jobUpdateList(condition, param, distribution, selectedStatus); + }); + } +} + +function jobQueryDistribution() { + if( $("#jobSelectDistribution").children().length == 0 ) { + queryDistribution( function(xml) { + // remove old select options + $("#jobSelectDistribution").empty(); + $("#jobSelectDistribution").append(""); + + $(xml).find("Data").find("DistributionName").each(function(){ + var name = $(this).text(); + + $("#jobSelectDistribution").append(""); + }); + + /* default distribution selection */ + $("#jobSelectDistribution option:eq(0)").attr("selected", "selected"); + $("#jobSelectDistribution").selectmenu('refresh'); + + selectJobAll(); + }); + } else { + jobsSearchSelected(); + } +} + +function queryJobList(requestUrl, condition, param, queryDistribution, selectedStatus, jobId) { + var url = ""; + if(condition == "all") { + url = requestUrl+"/"+condition+"/"+queryDistribution+"/"+selectedStatus+"/"+jobId; + } + else { + url = requestUrl+"/"+condition+"/"+param+"/"+queryDistribution+"/"+selectedStatus+"/"+jobId; + } + console.log("url :"+url); + return queryJobsList(url, function(xml) { + var lastJobId = 0; + $(xml).find("JobList").find("Job").each(function(){ + var id = $(this).find("Id").text(); + var li = generateHtmlJobList($(this)); + + lastJobId = id; + $("#jobList").append(li).listview('refresh'); + + $("#jobs-li-link-"+id).popupWindow({ + height:900, + width:800, + top:30, + left:50 + }); + }); + + console.log("last job id :"+lastJobId); + if(lastJobId > 0) + { + var moreJobListUrl = 'queryJobList("'+requestUrl+'", "'+condition+'", "'+param+'", "'+queryDistribution+'", "'+selectedStatus+'", "'+lastJobId+'")'; + console.log(moreJobListUrl); + $('#moreJobList').attr("onClick", moreJobListUrl); + $('#moreJobList').removeClass('ui-disabled'); + } + else + { + $('#moreJobList').addClass('ui-disabled'); + } + $('#moreJobList').button('refresh'); + + }, errorProcess); +} + +function jobUpdateList(condition, param, distribution, selectedStatus) { + var latest_job_id= $("#jobList li").first().attr("title"); + if(latest_job_id == undefined) { + latest_job_id = 0; + } + var working_job_list = searchWorkingList(); + item = {"Condition":condition, "Param":param, "Distribution":distribution, "Status":selectedStatus, "LatestId":latest_job_id, "WorkingJobId":working_job_list}; + + var update_ajax = updateList(item, function(xml) { + var firstLi= $("#jobList li").first(); + + // Add new job list + $(xml).find("JobList").find("Job").each(function(){ + var id = $(this).find("Id").text(); + var li = generateHtmlJobList($(this)); + + $(li).insertBefore(firstLi); + $("#jobList").listview('refresh'); + $("#jobs-li-link-"+id).popupWindow({ + height:900, + width:800, + top:30, + left:50 + }); + console.log("ADD List :"+id); + }); + + // Update working job list + $(xml).find("WorkingJobList").find("Job").each(function(){ + var job_id = $(this).find("Id").text(); + var parent_job_id = $(this).find("ParentJobId").text(); + var job_status = $(this).find("Status").text(); + var start_time = $(this).find("StartTime").text(); + var end_time = $(this).find("EndTime").text(); + var font_color = "black"; + + switch(job_status) + { + case "ERROR" : + case "CANCELED" : + font_color = "red"; + break; + case "INITIALIZING" : + case "JUST_CREATED" : + case "PENDING" : + case "WORKING" : + case "REMOTE_WORKING" : + font_color = "blue"; + break; + case "WAITING" : + font_color = "green"; + break; + case "FINISHED" : + font_color = "black"; + break; + default: + console.error(job_status+" status is not define."); + font_color = "black"; + break; + } + + // in case, exist job list element or not exist + if($("#jobs-li-"+job_id).length != 0) { + var html_status = ''+job_status+''; + var html_time = 'TIME: ' +start_time+ ' ~ '+end_time+ ''; + $("#jobs-li-status-"+job_id).html(html_status); + $("#jobs-li-time-"+job_id).html(html_time); + console.log("UPDATE List :"+job_id); + } + else { + var next_parent_job_li = $("#jobs-li-"+parent_job_id).nextAll(".jobs-li-header").first(); + console.log(parent_job_id); + console.log($("#jobs-li-"+parent_job_id)); + console.log(next_parent_job_li); + console.log(next_parent_job_li.text()); + + var li = generateHtmlJobList($(this)); + + $(li).insertBefore(next_parent_job_li); + $("#jobList").listview('refresh'); + $("#jobs-li-link-"+job_id).popupWindow({ + height:900, + width:800, + top:30, + left:50 + }); + console.log("ADD child list :"+job_id); + } + }); + }); + + var idx = addRequestList(update_ajax); + update_ajax.done(function() { + console.log("update complete. retry in 2 sec."); + setTimeout(function(){ + if(isPolling(idx) && $.mobile.activePage.attr('id') == "jobs" && idx < 900) { + console.log("Update request."); + request_list[idx].polling = false; + jobUpdateList(condition, param, distribution, selectedStatus); + } + else { + console.log("Stop update."); + } + }, 2000); + }); +} + +function clearSuggestJobSearchList() { + $("#jobSearchList").empty(); +} + +function jobsQueryGroupList() { + suggestion_list = []; + + queryAllGroup( function(xml) { + var idx = 0; + + $(xml).find("Data").find("GroupName").each(function(){ + suggestion_list[idx]= $(this).text(); + idx++; + }); + }, errorProcess); +} + +function suggestJobSearchList(inputText) { + var sugList = $("#jobSearchList"); + + if(inputText.length < 1) { + sugList.html(""); + sugList.listview("refresh"); + } else { + var str = ""; + var suggestion = ""; + for(var i=0, len=suggestion_list.length; i= 0) + { + str += "
  • "+suggestion+"
  • "; + } + } + sugList.html(str); + sugList.listview("refresh"); + } +} + +function jobSuggestListClick(suggestText) { + $("#jobSearchInputText").val(suggestText); + $("#jobSearchList").empty(); + + var startIndex = suggestText.search(/\[/); + var endIndex = suggestText.search('\]'); + + if(startIndex > 0 && endIndex >0) { + project = suggestText.substr(0, startIndex); + distribution = suggestText.substr(startIndex+1, endIndex-startIndex-1); + queryJobListProject(distribution, project); + } + else { + searchJob(suggestText); + } +} + +function jobsQueryProjectsList() { + var distribution = $("#jobSelectDistribution option:selected").val(); + suggestion_list = []; + + if(distribution == "ALL") { + queryAllProject(function(xml) { + var idx = 0; + + $(xml).find("Data").find("Project").each(function(){ + var projectName = $(this).find("Name").text(); + var distName = $(this).find("DistName").text(); + suggestion_list[idx]= projectName+"["+distName+"]" + idx++; + }); + }); + } + else { + queryProjectList(distribution, function(xml) { + var idx = 0; + + $(xml).find("Data").find("Project").each(function(){ + suggestion_list[idx]= $(this).find("ProjectName").text(); + idx++; + }); + }); + } +} + +function jobsClear() { + $("#jobSelectDistribution").empty(); + clearJobList(); +} + +function generateHtmlJobList(xml) { + var id = xml.find("Id").text(); + var distribution = xml.find("Distribution").text(); + var projectName = xml.find("ProjectName").text(); + var jobType = xml.find("JobType").text(); + var jobAttribute = xml.find("JobAttribute").text(); + var os = xml.find("Os").text(); + var jobStatus = xml.find("Status").text(); + var userName = xml.find("UserName").text(); + var startTime = xml.find("StartTime").text(); + var endTime = xml.find("EndTime").text(); + var li = ""; + var font_color = "black"; + + switch(jobStatus) + { + case "ERROR" : + case "CANCELED" : + font_color = "red"; + break; + case "INITIALIZING" : + case "JUST_CREATED" : + case "PENDING" : + case "WORKING" : + case "REMOTE_WORKING" : + font_color = "blue"; + break; + case "WAITING" : + font_color = "green"; + break; + case "FINISHED" : + font_color = "black"; + break; + default: + console.error(job_status+" status is not define."); + font_color = "black"; + break; + } + + if(jobAttribute == "SINGLE") + { + li = '
  • ' + + ''+id+ ' ' +projectName+ ''+distribution+'
  • ' + + '
  • ' + + '' + + '

    ' +projectName+ '

    ' + + '

    ID : ' +id+ '

    ' + + '

    TYPE : ' +jobType+ '

    ' + + '

    OS : ' + os + '

    ' + + '

    USER : ' +userName+ '

    ' + + '

    TIME: ' +startTime+ ' ~ '+endTime+ '

    '; + + li = li + '

    '+jobStatus+'

    '; + li = li + ''; + li = li + '
  • '; + } + else if(jobAttribute == "MULTI") + { + li = '
  • ' + + ''+id+ ' ' +jobAttribute+ ''+distribution+'
  • ' + + '
  • ' + + '' + + '

    USER : ' +userName+ '

    ' + + '

    TIME: ' +startTime+ ' ~ '+endTime+ '

    '; + + ''; + + li = li + '

    '+jobStatus+'

    '; + li = li + ''; + li = li + '
  • '; + } + else if(jobAttribute == "CHILD") + { + li = '
  • ' + + '' + + '

    ' +projectName+ '

    ' + + '

    ID : ' +id+ '

    ' + + '

    TYPE : ' +jobType+ '

    ' + + '

    OS : ' +os+ '

    ' + + '

    TIME: ' +startTime+ ' ~ '+endTime+ '

    '; + + ''; + + li = li + '

    '+jobStatus+'

    '; + li = li + ''; + li = li + '
  • '; + } + + return li; +} + +function searchWorkingList() { + working_job_array = new Array(); + $("#jobList .jobs-list-data").each(function(index) { + var job_attr = $(this).find(".jobs-li-hidden-attr").text(); + var job_id = $(this).find(".jobs-li-hidden-id").text(); + var job_status= $(this).find(".jobs-li-status").text(); + switch(job_status) + { + case "INITIALIZING" : + case "JUST_CREATED" : + case "PENDING" : + case "WORKING" : + case "REMOTE_WORKING" : + case "WAITING" : + if(job_attr != "CHILD") { + working_job_array.push(job_id); + } + break; + default: + break; + } + }); + return working_job_array; +} + +/* For background update AJAX */ +function classAjax() { + var polling = false; + var ajaxObj = undefined; +} + +function addRequestList(ajaxObj) { + var class_ajax = new classAjax(); + class_ajax.polling = true; + class_ajax.ajaxObj = ajaxObj; + + return request_list.push(class_ajax) - 1; +} + +function clearRequestList() { + while(request_list.length > 0) { + var last_request = request_list[request_list.length - 1]; + if(last_request.polling == true && last_request.ajaxObj != undefined) { + last_request.ajaxObj.abort(); + } + request_list.pop(); + } + console.log("Clear all update."); +} + +function isPolling(idx) { + if(request_list[idx] != undefined) { + return request_list[idx].polling; + } + else { + return false; + } +} + diff --git a/dibs-web/public/javascripts/log.js b/dibs-web/public/javascripts/log.js new file mode 100644 index 0000000..607b68c --- /dev/null +++ b/dibs-web/public/javascripts/log.js @@ -0,0 +1,184 @@ +/* + log.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +var request; +var stop = 1; + +var baseUrl; +var job_id; +var last_line = 0; + +var init = function () { + console.log("init() called"); + var myUrl = location.href; + var varCut = myUrl.indexOf("?"); + var varCheck = myUrl.substring(varCut+1); + eval(varCheck); + + job_id = jobid; + $('#job-info-id').text(job_id); + console.log(job_id); + + var baseCut = myUrl.indexOf("log.html"); + + baseUrl = myUrl.substring(0, baseCut); + console.log("base url:"+baseUrl); + + queryLog(); +}; + +$(document).ready(init); + +function queryLog() +{ + var next_line = Number(last_line) + 1; + queryJobsLog(job_id, next_line, function(xml) { + /* pre-process for header */ + job_id = $(xml).find("Data").find("JobId").text(); + var distribution = $(xml).find("Data").find("Distribution").text(); + var project = $(xml).find("Data").find("Project").text(); + var builder = $(xml).find("Data").find("Builder").text(); + var job_status = $(xml).find("Data").find("Status").text(); + var time = $(xml).find("Data").find("Time").text(); + var conti = Number($(xml).find("Data").find("Continue").text()); + var working_status = 0; + + $('#job-info-distribution').html(distribution); + $('#job-info-project').html(project); + $('#job-info-builder').html(builder); + $('#job-info-status').html(job_status); + switch(job_status) + { + case "ERROR" : + case "CANCELED" : + $('#job-info-status').css("color","red"); + $('#job-info-cancel input').attr("disabled", true); + working_status = 0; + break; + case "INITIALIZING" : + case "JUST_CREATED" : + case "PENDING" : + case "WORKING" : + case "REMOTE_WORKING" : + $('#job-info-status').css("color","blue"); + working_status = 1; + break; + case "WAITING" : + $('#job-info-status').css("color","green"); + working_status = 1; + break; + case "FINISHED" : + $('#job-info-status').css("color","black"); + $('#job-info-cancel input').attr("disabled", true); + working_status = 0; + break; + default: + console.error(job_status+" status is not define."); + $('#job-info-cancel input').attr("disabled", true); + working_status = 0; + break; + } + + /* Insert data */ + $(xml).find("Data").find("LogData").each(function() { + var insertTable = document.getElementById("logTable"); + var insertRow = document.createElement("tr"); + var insertCel1 = document.createElement("td"); + var insertCel2 = document.createElement("td"); + + var line_number = $(this).attr("Line"); + var line_data = $(this).text(); + + insertCel1.width = '30'; + insertCel1.style.textAlign = 'right'; + insertCel1.style.cellpacing = '5'; + insertCel1.innerHTML = line_number; + last_line = line_number; + + insertCel2.style.textAlign = 'left'; + insertCel2.innerHTML = line_data; + + insertRow.appendChild(insertCel1); + insertRow.appendChild(insertCel2); + + insertTable.appendChild(insertRow); + + }); + + if(working_status == 1){ + console.log("scroll"); + scrollToBottom(); + } + + autoQueryLog(conti, working_status); + }); +} + +function autoQueryLog(conti, working_status) { + if(conti && stop) { + queryLog(); + } + + if(working_status == 1 && stop) { + console.log("status is working. try request"); + setTimeout(function(){queryLog()}, 3000); + } +} + +function stopLog() { + stop = 0; +} + +function moreLog() { + stop = 1; + queryLog(); +} + +function cancelJob() { + cancelJobsJobid(job_id, function(xml) { + alert("Reqeusted cancel job : "+job_id); + }); +} + +function getYScroll() +{ + var yScroll; + if (window.innerHeight && window.scrollMaxY) { + yScroll = window.innerHeight + window.scrollMaxY; + } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac + yScroll = document.body.scrollHeight; + } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari + yScroll = document.body.offsetHeight; + } + return yScroll; +} + +function scrollToBottom() { + window.scrollTo(0,getYScroll()); +} + diff --git a/dibs-web/public/javascripts/main.js b/dibs-web/public/javascripts/main.js new file mode 100644 index 0000000..bb60f7f --- /dev/null +++ b/dibs-web/public/javascripts/main.js @@ -0,0 +1,337 @@ +/* + main.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +//Initialize function +var baseUrl = ""; + +var init = function () { + console.log("init() called"); + var myUrl = location.href; + var urlCut = myUrl.indexOf("index.html"); + var sharpCut = myUrl.indexOf("#"); + + if(urlCut > 0) { + baseUrl = myUrl.substring(0, urlCut); + } + else if(sharpCut > 0) { + baseUrl = myUrl.substring(0, sharpCut); + } + else { + baseUrl = window.location.href; + } + console.log("base url:"+baseUrl); +}; +$(document).ready(init); + +$.ajaxSetup({ + beforeSend: function(xhr) { + xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content')); + } +}); + + +$( document ).bind( "pageshow", function( event, data ){ +}); + +$( document ).bind( "pagechange", function( event, data ){ + var id = $.mobile.activePage.attr('id'); + + switch(id){ + case "index": + generateNavigationBar(id); + break; + case "signup": + clearFormData('signupForm'); + break; + case "login": + clearFormData('loginForm'); + break; + case "projects": + generateNavigationBar(id); + projectsInit(id); + break; + case "build": + generateNavigationBar(id); + buildInit(id); + break; + case "jobs": + generateNavigationBar(id); + jobsInit(); + break; + case "log": + logInit(); + break; + case "adminUser": + generateNavigationBar(id); + adminUserInit(); + break; + case "adminGroup": + generateNavigationBar(id); + adminGroupInit(); + break; + case "adminServer": + generateNavigationBar(id); + adminServerInit(); + break; + case "adminProject": + generateNavigationBar(id); + adminProjectInit(); + break; + case "adminDistribution": + generateNavigationBar(id); + adminDistributionInit(); + break; + case "modifyDistribution": + adminDistributionModifyPopupInit(); + break; + case "adminUserModifyPopup": + adminUserModifyPopupInit(); + break; + case "modifyBinaryProject": + adminProjectModifyBinaryProjectInit(); + break; + case "modifyGitProject": + adminProjectModifyGitProjectInit(); + break; + case "adminGroupAddPopup": + adminGroupAddInit(); + break; + case "adminGroupModifyPopup": + adminGroupModifyInit(); + break; + case "adminServerModifyRemoteBuildServer": + adminServerModifyRemoteBuildServerInit(); + break; + case "addGitProject": + adminProjectAddGitInit(); + break; + case "addBinaryProject": + adminProjectAddBinaryInit(); + break; + case "adminServerAddSupportedOs": + adminServerAddSupportedOsInit(); + break; + case "adminServerRemoveOSCategory": + adminServerRemoveOSCategoryInit(); + break; + case "signup": + //queryGroupListForSignup(); + break; + default: + break; + } + + // Call check session info + checkSessionInfo(); +}); + +$( document ).bind( "mobileinit", function() { + $.support.cors = true; + $.support.cors = true; + $.mobile.pushStateEnabled = false; +}); + +function clearFormData(elementId){ + $("#"+elementId).find(':input').each(function() { + switch(this.type) { + case 'text': + case 'password': + case 'select-multiple': + case 'select-one': + case 'textarea': + $(this).val(''); + break; + case 'checkbox': + case 'radio': + $(this).checked = false; + break; + } + }); +} + +function generateNavigationBar(id) { + if(sessionStorage.sessionInfoEmail) + { + var admin = sessionStorage.sessionInfoAdmin; + if(admin == "TRUE") + { + generateNavigationBarAdmin(id); + } + else + { + generateNavigationBarUser(id); + } + } else { + $("#"+id+"-navigationBar").empty(); + } +} + +function generateNavigationBarUser(id) { + var naviHtml = "" + naviHtml = '
  • BUILD
  • '; + switch(id){ + case "projects": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + break; + case "build": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + break; + case "jobs": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + break; + default: + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + break; + } + + $("#"+id+"-navigationBar").empty(); + $("#"+id+"-navigationBar").append(naviHtml).listview("refresh"); +} + +function generateNavigationBarAdmin(id) { + var naviHtml = "" + naviHtml = '
  • BUILD
  • '; + switch(id){ + case "projects": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "build": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "jobs": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "adminUser": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "adminGroup": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "adminServer": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "adminDistribution": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + case "adminProject": + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + default: + naviHtml += '
  • Projects
  • '; + naviHtml += '
  • Build
  • '; + naviHtml += '
  • Jobs
  • '; + naviHtml += '
  • ADMIN
  • '; + naviHtml += '
  • User
  • '; + naviHtml += '
  • Group
  • '; + naviHtml += '
  • Server
  • '; + naviHtml += '
  • Distribution
  • '; + naviHtml += '
  • Project
  • '; + break; + } + + $("#"+id+"-navigationBar").empty(); + $("#"+id+"-navigationBar").append(naviHtml).listview("refresh"); +} + +function dibsWebClear(){ + projectsClear(); + buildClear(); + jobsClear(); +} diff --git a/dibs-web/public/javascripts/popup-window.js b/dibs-web/public/javascripts/popup-window.js new file mode 100644 index 0000000..fd5d493 --- /dev/null +++ b/dibs-web/public/javascripts/popup-window.js @@ -0,0 +1,90 @@ +/* + popup-window.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +(function($){ + $.fn.popupWindow = function(instanceSettings){ + + return this.each(function(){ + + $(this).click(function(){ + + $.fn.popupWindow.defaultSettings = { + centerBrowser:0, // center window over browser window? {1 (YES) or 0 (NO)}. overrides top and left + centerScreen:0, // center window over entire screen? {1 (YES) or 0 (NO)}. overrides top and left + height:500, // sets the height in pixels of the window. + left:0, // left position when the window appears. + location:1, // determines whether the address bar is displayed {1 (YES) or 0 (NO)}. + menubar:0, // determines whether the menu bar is displayed {1 (YES) or 0 (NO)}. + resizable:0, // whether the window can be resized {1 (YES) or 0 (NO)}. Can also be overloaded using resizable. + scrollbars:1, // determines whether scrollbars appear on the window {1 (YES) or 0 (NO)}. + status:0, // whether a status line appears at the bottom of the window {1 (YES) or 0 (NO)}. + width:500, // sets the width in pixels of the window. + windowName:null, // name of window set from the name attribute of the element that invokes the click + windowURL:null, // url used for the popup + top:0, // top position when the window appears. + toolbar:0 // determines whether a toolbar (includes the forward and back buttons) is displayed {1 (YES) or 0 (NO)}. + }; + + settings = $.extend({}, $.fn.popupWindow.defaultSettings, instanceSettings || {}); + + var windowFeatures = 'height=' + settings.height + + ',width=' + settings.width + + ',toolbar=' + settings.toolbar + + ',scrollbars=' + settings.scrollbars + + ',status=' + settings.status + + ',resizable=' + settings.resizable + + ',location=' + settings.location + + ',menuBar=' + settings.menubar; + + settings.windowName = this.name || settings.windowName; + settings.windowURL = this.href || settings.windowURL; + var centeredY,centeredX; + + if(settings.centerBrowser){ + + if ($.browser.msie) {//hacked together for IE browsers + centeredY = (window.screenTop - 120) + ((((document.documentElement.clientHeight + 120)/2) - (settings.height/2))); + centeredX = window.screenLeft + ((((document.body.offsetWidth + 20)/2) - (settings.width/2))); + }else{ + centeredY = window.screenY + (((window.outerHeight/2) - (settings.height/2))); + centeredX = window.screenX + (((window.outerWidth/2) - (settings.width/2))); + } + window.open(settings.windowURL, settings.windowName, windowFeatures+',left=' + centeredX +',top=' + centeredY).focus(); + }else if(settings.centerScreen){ + centeredY = (screen.height - settings.height)/2; + centeredX = (screen.width - settings.width)/2; + window.open(settings.windowURL, settings.windowName, windowFeatures+',left=' + centeredX +',top=' + centeredY).focus(); + }else{ + window.open(settings.windowURL, settings.windowName, windowFeatures+',left=' + settings.left +',top=' + settings.top).focus(); + } + return false; + }); + + }); + }; +})(jQuery); diff --git a/dibs-web/public/javascripts/post-process.js b/dibs-web/public/javascripts/post-process.js new file mode 100644 index 0000000..0c0aaaa --- /dev/null +++ b/dibs-web/public/javascripts/post-process.js @@ -0,0 +1,35 @@ +/* + post-process.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +$('.uploadWindow').popupWindow({ + height:200, + width:400, + top:50, + left:50 +}); + diff --git a/dibs-web/public/javascripts/projects.js b/dibs-web/public/javascripts/projects.js new file mode 100644 index 0000000..2288665 --- /dev/null +++ b/dibs-web/public/javascripts/projects.js @@ -0,0 +1,178 @@ +/* + projects.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +function projectsInit() { + if( $("#projects-select-distribution").children().length == 0 ) { + queryDistribution( function(xml) { + // remove old select options + $("#projects-select-distribution").empty(); + + $(xml).find("Data").find("DistributionName").each(function(){ + var name = $(this).text(); + + $("#projects-select-distribution").append(""); + }); + + /* default distribution selection */ + $("#projects-select-distribution option:eq(0)").attr("selected", "selected"); + $("#projects-select-distribution").selectmenu('refresh'); + + projectsQueryProjectListType("MY"); + $('#projectsSearchSelect input[type="radio"]').attr("checked",false).checkboxradio("refresh"); + $('#projectsSearchSelect input[type="radio"]:first').attr("checked",true).checkboxradio("refresh"); + }); + } else { + projectsQueryProjectListType("MY"); + $('#projectsSearchSelect input[type="radio"]').attr("checked",false).checkboxradio("refresh"); + $('#projectsSearchSelect input[type="radio"]:first').attr("checked",true).checkboxradio("refresh"); + } +} + +function projectsQueryProjectListType(queryType) { + var distName = $("#projects-select-distribution option:selected").val(); + + queryProjectsInfoInDistribution( distName, function(xml) { + var projectList = document.getElementById("projects-project-list"); + + /* remove all list */ + while(projectList.hasChildNodes()) + { + projectList.removeChild(projectList.firstChild); + } + + $(xml).find("Data").find("ProjectList").find("Project").each(function(){ + switch(queryType) { + case "ALL": + projectsAppendProjectList($(this), projectList); + break; + case "MY": + var groupAccess = $(this).find("GroupAccess").text(); + if(groupAccess == "TRUE") { + projectsAppendProjectList($(this), projectList); + } + break; + case "GIT": + var type = $(this).find("Type").text(); + if(type == "GIT") { + projectsAppendProjectList($(this), projectList); + } + break; + case "BINARY": + var type = $(this).find("Type").text(); + if(type == "BINARY") { + projectsAppendProjectList($(this), projectList); + } + break; + default: + ; + } + }); + + $('.projects-project-list-collapsible').collapsible(); + $('.projects-project-list-listview').listview(); + + }); +} + +function projectsAppendProjectList( project, projectList ) { + var name = project.find("Name").text(); + var type = project.find("Type").text(); + var groupAccess = project.find("GroupAccess").text(); + var maintainer = project.find("Maintainer").text(); + + var div = document.createElement('div'); + div.setAttribute('data-role', 'collapsible'); + div.setAttribute('data-inset', 'false'); + div.setAttribute('data-mini', 'true'); + div.setAttribute('class', 'projects-project-list-collapsible'); + if(groupAccess == "TRUE") { + div.setAttribute('data-theme', "b"); + } + else { + div.setAttribute('data-theme', "d"); + } + projectList.appendChild(div); + + var h2 = document.createElement('h2'); + h2.innerHTML = ''+name+''+type+''; + div.appendChild(h2); + + var infoLine = document.createElement('p'); + infoLine.setAttribute('style', 'font-size: 12px'); + infoLine.innerHTML = "Maintainer : "+maintainer+"

    "; + div.appendChild(infoLine); + + var ul = document.createElement('ul'); + ul.setAttribute('data-role', "listview"); + ul.setAttribute('class', "projects-project-list-listview"); + ul.setAttribute('data-filter-theme', "c"); + ul.setAttribute('data-divider-theme', "c"); + div.appendChild(ul); + + project.find("ProjectOs").each(function(){ + var osName = $(this).find("OsName").text(); + + var li = document.createElement('li'); + li.setAttribute('style', 'font-size: 12px'); + li.setAttribute('data-role', "list-divider") + li.innerHTML = ""+osName+""; + ul.appendChild(li); + + $(this).find("Package").each(function(){ + var packageName = $(this).find("PackageName").text(); + var packageVersion = $(this).find("PackageVersion").text(); + var startTime = $(this).find("StartTime").text(); + var endTime= $(this).find("EndTime").text(); + var userName = $(this).find("UserName").text(); + var userEmail = $(this).find("UserEmail").text(); + + if(packageName != "") + { + var liInfo = document.createElement('li'); + liInfo.setAttribute('style', 'font-size: 12px'); + var info = "

    "+packageName+" "+packageVersion+"

    "; + info += "

    Lastest build time : "+startTime+" ~ "+endTime+"

    "; + info += "

    Lastest build user : "+userName+" ["+userEmail+"]

    "; + liInfo.innerHTML = info; + ul.appendChild(liInfo); + } + }); + }); +} + +function projectsClear() { + $("#projects-select-distribution").empty(); + + var projectList = document.getElementById("projects-project-list"); + /* remove all list */ + while(projectList.hasChildNodes()) + { + projectList.removeChild(projectList.firstChild); + } +} + diff --git a/dibs-web/public/javascripts/session.js b/dibs-web/public/javascripts/session.js new file mode 100644 index 0000000..12582dc --- /dev/null +++ b/dibs-web/public/javascripts/session.js @@ -0,0 +1,146 @@ +/* + session.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +$(function() { + $('#login-password').keypress(function() { + if(event.keyCode == '13') { + sessionLogin(); + } + }); +}); + +function setSessionInfo(xml){ + var email = $(xml).find("Header").find("UserInfo").find("Email").text(); + var name = $(xml).find("Header").find("UserInfo").find("Name").text(); + var admin = $(xml).find("Header").find("UserInfo").find("Admin").text(); + var group; + var idx = 0; + $(xml).find("Header").find("UserInfo").find("GroupList").find("Group").each(function() { + if(idx == 0) { + group = $(this).find("GroupName").text(); + } + }); + + sessionStorage.sessionInfoEmail = email; + sessionStorage.sessionInfoName = name; + sessionStorage.sessionInfoGroup = group; + sessionStorage.sessionInfoAdmin = admin; + + checkSessionInfo(); +} + +function checkSessionInfo(){ + var email = sessionStorage.sessionInfoEmail; + var name = sessionStorage.sessionInfoName; + + if(email) + { + sessionHtml = '
    '; + sessionHtml += '

    '+name+' | '; + sessionHtml += 'Log out

    '; + } + else + { + sessionHtml = '
    '; + sessionHtml += 'Sign up'; + sessionHtml += 'Log in
    '; + } + $(".sessionInfo").html(sessionHtml).trigger("create"); +} + +function clearSessionInfo(){ + sessionStorage.sessionInfoEmail = ""; + sessionStorage.sessionInfoName = ""; + sessionStorage.sessionInfoGroup = ""; + sessionStorage.sessionInfoAdmin = ""; +} + +function expireSession(){ + clearSessionInfo(); + $.mobile.changePage("index.html"); + generateNavigationBar("index"); +} + +function sessionLogin() { + var infoList = []; + var infoItem; + var email = $('#login-email').val(); + var password = $('#login-password').val(); + + if(email == ""){ + alert("Email is invalid"); + return false; + } + + if(password == ""){ + alert("Password is invalid"); + return false; + } + + changeInfoItem = {"Type":"ModifyUser", "Email":email, "Password":password }; + infoList.push(changeInfoItem); + + login(infoList, function (json) { + var result = json.Result; + var message = json.UserInfo.Message; + var email = json.UserInfo.Eamil; + var name = json.UserInfo.Name; + var group = json.UserInfo.GroupName; + var admin = json.UserInfo.Admin; + + if(result == "SUCCESS") + { + sessionStorage.sessionInfoEmail = email; + sessionStorage.sessionInfoName = name; + sessionStorage.sessionInfoGroup = group; + sessionStorage.sessionInfoAdmin = admin; + $.mobile.changePage("#index"); + } + else + { + alert(message); + } + }); +} + +function signupQueryGroupList() { + queryAllGroup( function(xml) { + $("#applyGroupRadio").children().remove(); + + var newHtml ='
    Apply for group :'; + $(xml).find("Data").find("GroupName").each(function(){ + var name = $(this).text(); + newHtml += ''; + newHtml += ''; + }); + console.log(newHtml); + $("#applyGroupRadio").append(newHtml).trigger('create'); + $("#applyGroupRadio div[role='heading']").attr("style","text-align: left; font-size: 12px"); + }, errorProcess); +} + diff --git a/dibs-web/public/javascripts/user.js b/dibs-web/public/javascripts/user.js new file mode 100644 index 0000000..70d8689 --- /dev/null +++ b/dibs-web/public/javascripts/user.js @@ -0,0 +1,136 @@ +/* + user.js + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Sungmin Kim +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +*/ + +$(function() { + $('#user-password-confirmation').keypress(function() { + if(event.keyCode == '13') { + userSignUp(); + } + }); +}); + +function userSignUp() { + var infoList = []; + var infoItem; + var email = $('#user-email').val(); + var name = $('#user-name').val(); + var password = $('#user-password').val(); + var password_confirm = $('#user-password-confirmation').val(); + var emailCheckReg = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/ + + if(email == "" || !emailCheckReg.test(email)){ + alert("Email is invalid"); + return false; + } + if(name == ""){ + alert("Name is invalid"); + return false; + } + + if(password == ""){ + alert("Password is invalid"); + return false; + } + + if(password != password_confirm){ + alert("Password is different"); + return false; + } + + changeInfoItem = {"Type":"ModifyUser", "Email":email, "Name":name, "Password":password, "PasswordConfirm":password_confirm}; + infoList.push(changeInfoItem); + + signUp(infoList, function (json) { + alert("Success sign up"); + + var result = json.Result; + var message = json.UserInfo.Message; + var email = json.UserInfo.Eamil; + var name = json.UserInfo.Name; + var group = json.UserInfo.GroupName; + var admin = json.UserInfo.Admin; + + if(result == "SUCCESS") + { + sessionStorage.sessionInfoEmail = email; + sessionStorage.sessionInfoName = name; + sessionStorage.sessionInfoGroup = group; + sessionStorage.sessionInfoAdmin = admin; + $.mobile.changePage("#index"); + } + else + { + alert(message); + } + }); +} + +function userQueryUserInfo() { + queryUserInfo( function (xml) { + var email = $(xml).find("Data").find("User").find("Email").text(); + var name = $(xml).find("Data").find("User").find("Name").text(); + + $("#popup-user-info-email").val(email).textinput(); + $("#popup-user-info-name").val(name).textinput(); + }); +} + +function userModifyUserInfo() { + var changeInfoList = []; + var changeInfoItem; + var email = $('#popup-user-info-email').val(); + var name = $('#popup-user-info-name').val(); + var password = $('#popup-user-info-password').val(); + var password_confirm = $('#popup-user-info-password-confirm').val(); + + if(email == ""){ + alert("Email is invalid"); + return false; + } + if(name == ""){ + alert("Name is invalid"); + return false; + } + + if(password == ""){ + alert("Password is invalid"); + return false; + } + + if(password != password_confirm){ + alert("Password is different"); + return false; + } + + changeInfoItem = {"Type":"ModifyUser", "Email":email, "Name":name, "Password":password, "PasswordConfirm":password_confirm}; + changeInfoList.push(changeInfoItem); + + modifyUserInfo(changeInfoList, function (xml) { + alert("Success changed information"); + }); +} diff --git a/dibs-web/public/log.html b/dibs-web/public/log.html new file mode 100644 index 0000000..ec4c4c4 --- /dev/null +++ b/dibs-web/public/log.html @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + +
    + +
    + + + + diff --git a/dibs-web/public/robots.txt b/dibs-web/public/robots.txt new file mode 100644 index 0000000..085187f --- /dev/null +++ b/dibs-web/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-Agent: * +# Disallow: / diff --git a/dibs-web/public/stylesheets/application.css b/dibs-web/public/stylesheets/application.css new file mode 100644 index 0000000..3192ec8 --- /dev/null +++ b/dibs-web/public/stylesheets/application.css @@ -0,0 +1,13 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require_tree . + */ diff --git a/dibs-web/public/stylesheets/images/ajax-loader.gif b/dibs-web/public/stylesheets/images/ajax-loader.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd1a189c21fed1c7ba00c4bb4fad407bd6d1e5f9 GIT binary patch literal 7825 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 LHfkt@K(PD|Q{O_Y literal 0 HcmV?d00001 diff --git a/dibs-web/public/stylesheets/images/ajax-loader.png b/dibs-web/public/stylesheets/images/ajax-loader.png new file mode 100644 index 0000000000000000000000000000000000000000..13b208dddd67f65dc5af0f6ed1a8c8227e458ed3 GIT binary patch literal 340 zcmeAS@N?(olHy`uVBq!ia0vp^${@_a0wlLwsJ_I&z$oqM;uum9_w;IF?hyl?wugZn ztpNtD0YDOrf;3te2XIZbW7)oR^P8_SYp?yv-nGYmvE9_gA%B19b2GLaNg3_ z_+-yZY2(+KQHf2HU!Jj9-t&9+>bzs~jlS-#=2=+ukx#hA^ZPL^_Mm`0`tyuATb22l z&b>I4!eXD~^vRLqR*ki>5trYQwvIK80z885r(&F6c04(_LbHfhwR4B?li$fvEq6NF kG9=9t`9lQN?q%09so1H6%o2#w1BMoZr>mdKI;Vst0DAt0R{#J2 literal 0 HcmV?d00001 diff --git a/dibs-web/public/stylesheets/images/icons-18-black.png b/dibs-web/public/stylesheets/images/icons-18-black.png new file mode 100644 index 0000000000000000000000000000000000000000..ce1b758ad580663f92c36fb0902afb677ded0b11 GIT binary patch literal 1767 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 literal 0 HcmV?d00001 diff --git a/dibs-web/public/stylesheets/images/icons-36-black.png b/dibs-web/public/stylesheets/images/icons-36-black.png new file mode 100644 index 0000000000000000000000000000000000000000..1a59d7c375d6611262a9ac86db23eb94570d7319 GIT binary patch literal 3611 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 A8~^|S literal 0 HcmV?d00001 diff --git a/dibs-web/public/upload.html b/dibs-web/public/upload.html new file mode 100644 index 0000000..fb2ffb0 --- /dev/null +++ b/dibs-web/public/upload.html @@ -0,0 +1,97 @@ + + + + + + +
    + + +
    + + + + + diff --git a/dibs-web/script/rails b/dibs-web/script/rails new file mode 100755 index 0000000..f8da2cf --- /dev/null +++ b/dibs-web/script/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' diff --git a/doc/DIBS_Advanced_Guide.pdf b/doc/DIBS_Advanced_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ae15e409eccbf0ae7fd411c4426523287ddac79f GIT binary patch literal 502611 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

    1DCxR02U!jo5wK>sClw3KS_b4z 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

    FzYrI>+5 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 literal 0 HcmV?d00001 diff --git a/doc/Tizen_SDK_Development_Guide.pdf b/doc/Tizen_SDK_Development_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f39ba84bfcb164a42a2ec685f4d70d4f15a03b14 GIT binary patch literal 168703 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

    =^EuC8sOe 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�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 a0D!9r+~s131pHX|Abgng^inF)nEwwwa&jL4 literal 0 HcmV?d00001 diff --git a/doc/Tizen_SDK_Package_Guide.pdf b/doc/Tizen_SDK_Package_Guide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b9d966eece99cf5f29c6b11dafa4ec80ade0c4e7 GIT binary patch literal 56301 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

    2%1;|xnBw?sD$3(;p>j(y5e$T6BAK+eN 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

    9tA}OO^0oHBhH~JIj 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 $BIN_DIR/VERSION } - - -$1 -echo "$1 success" - diff --git a/package/build.macos b/package/build.macos new file mode 100755 index 0000000..8264341 --- /dev/null +++ b/package/build.macos @@ -0,0 +1,26 @@ +#!/bin/sh -xe +# clean +clean() +{ + rm -rf $SRCDIR/*.zip + rm -rf $SRCDIR/*.tar.gz +} + +# build +build() +{ + echo "build" +} + +# install +install() +{ + BIN_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/tools/dibs/ + mkdir -p $BIN_DIR + cp -f $SRCDIR/pkg-* $BIN_DIR/ + cp -f $SRCDIR/build-* $BIN_DIR/ + cp -rf $SRCDIR/src $BIN_DIR/ + cp -rf $SRCDIR/dibs-web $BIN_DIR/ + cp -f $SRCDIR/upgrade $BIN_DIR/ + echo $VERSION > $BIN_DIR/VERSION +} diff --git a/package/build.windows b/package/build.windows new file mode 100755 index 0000000..8264341 --- /dev/null +++ b/package/build.windows @@ -0,0 +1,26 @@ +#!/bin/sh -xe +# clean +clean() +{ + rm -rf $SRCDIR/*.zip + rm -rf $SRCDIR/*.tar.gz +} + +# build +build() +{ + echo "build" +} + +# install +install() +{ + BIN_DIR=$SRCDIR/package/dibs.package.${TARGET_OS}/data/tools/dibs/ + mkdir -p $BIN_DIR + cp -f $SRCDIR/pkg-* $BIN_DIR/ + cp -f $SRCDIR/build-* $BIN_DIR/ + cp -rf $SRCDIR/src $BIN_DIR/ + cp -rf $SRCDIR/dibs-web $BIN_DIR/ + cp -f $SRCDIR/upgrade $BIN_DIR/ + echo $VERSION > $BIN_DIR/VERSION +} diff --git a/package/changelog b/package/changelog new file mode 100644 index 0000000..2ccd4aa --- /dev/null +++ b/package/changelog @@ -0,0 +1,109 @@ +* 2.0.10 +- hot fix No method error +== hyoun jiil 2013-01-23 +* 2.0.9 +- several bug fix +-- sync bug fix +== hyoun jiil 2013-01-21 +* 2.0.8 +- WEB +-- Update new job list and job's status in JOBS page. +-- Update added job log and job's status in JOBS LOG page. +== sungmin kim 2012-12-12 +* 2.0.7 +- several bug fix +- Add distribution to upgrade commnad +- WEB +-- Fixed bug about request url include dot +-- Support browser about IE9 +-- Add loading event (spinner) +== hyoun jiil 2012-12-24 +* 2.0.6 +- several internal bug fix +== hyoun jiil 2012-12-24 +* 2.0.5 +- Upgraded protocol version to 1.7.0 +- Fixed Sign Up bug on WEB +- Fixed Job group filtering on WEB +- Fixed User Delete on WEB +== donghee yang 2012-12-14 +* 2.0.4 +- Fixed below bug again +== donghee yang 2012-12-12 +* 2.0.3 +- fixed bug about web, build-cli query +== donghee yang 2012-12-12 +* 2.0.2 +- fixed bug about web, build-cli query +== sungmin kim 2012-12-12 +* 2.0.1 +- fixed build.os +== sungmin kim 2012-12-12 +* 2.0.0 +- DIBS web added. +== sungmin kim 2012-12-12 +* 1.2.20 +- display distribution when query +== hyoun jiil 2012-11-29 +* 1.2.19 +- Fixed remote build bug and undefined method bug +== donghee yang 2012-11-29 +* 1.2.18 +- Fixed "cancel" operation of remote job +== donghee yang 2012-11-29 +* 1.2.17 +- change dir seperator when windows remove script execute +== hyoun jiil 2012-11-28 +* 1.2.16 +- Increased communication timeout to 120sec +== donghee yang 2012-11-28 +* 1.2.15 +- Increased communication timeout to 60sec +== donghee yang 2012-11-28 +* 1.2.14 +- Applied basic job priority +- Old job directory will be cleaned when restart +== donghee yang 2012-11-28 +* 1.2.13 +- multi-build can passing --noreverse option +== hyoun jiil 2012-10-29 +* 1.2.12 +- Change log on working package server +== hyoun jiil 2012-10-29 +* 1.2.10 +- Change log support +== hyoun jiil 2012-10-29 +* 1.2.9 +- Added distribution lock +== hyoun jiil 2011-10-18 +* 1.2.8 +- Fixed "cancel" bug +- Changed to remain logss about communication errors +== hyoun jiil 2011-10-18 +* 1.2.7 +- Fixed a bug that reverse build choose wrong distribution project +== hyoun jiil 2011-10-17 +* 1.2.6 +- Increase TimeOut to 30 sec +== hyoun jiil 2011-10-17 +* 1.2.5 +- Fixed a bug that pkg-build is not working +- Fixed some bugs when using ruby1.9.1 +== hyoun jiil 2011-10-17 +* 1.2.4 +- Fixed a bug that "upgrade" of sub servers are not done +== hyoun jiil 2011-10-16 +* 1.2.3 +- Set "wget" retry count to 3 +== hyoun jiil 2011-10-16 +* 1.2.2 +- Fixed server log contents +== hyoun jiil 2011-10-16 +* 1.2.1 +- Fixed some bugs : upgrade, parse error handling, pkg-build, remote job cancel +== hyoun jiil 2011-10-16 +* 1.2.0 +- change log support +- db support +- multiple distribution support +== hyoun jiil 2012-10-16 diff --git a/package/pkginfo.manifest b/package/pkginfo.manifest index 3e172a3..60bd43c 100644 --- a/package/pkginfo.manifest +++ b/package/pkginfo.manifest @@ -1,15 +1,8 @@ -Package : dibs -Version : 0.20.7 -Maintainer : taejun ha, jiil hyoun , , donghee yang< donghee.yang@samsung.com > -Description : Distribute Inteligent Build System -OS : linux -Build-host-os : linux Source : dibs +Version :2.0.10 +Maintainer : taejun ha, jiil hyoun , donghyuk yang , donghee yang , sungmin kim , jiil hyoun , , donghee yang< donghee.yang@samsung.com > +OS : ubuntu-32, ubuntu-64, windows-32, windows-64, macos-64 +Build-host-os : ubuntu-32 Description : Distribute Inteligent Build System -OS : windows -Build-host-os : linux -Source : dibs diff --git a/package/pkginfo.manifest.local b/package/pkginfo.manifest.local new file mode 100644 index 0000000..ce655de --- /dev/null +++ b/package/pkginfo.manifest.local @@ -0,0 +1,6 @@ +Include: pkginfo.manifest + +Package : dibs +OS : ubuntu-32, windows-32, macos-64, ubuntu-64, windows-64 +Build-host-os : windows-32, macos-64, ubuntu-64, windows-64 +Description : Distribute Inteligent Build System diff --git a/pkg-build b/pkg-build index 22b58f3..dbfad26 100755 --- a/pkg-build +++ b/pkg-build @@ -1,7 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby =begin - + pkg-build Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -36,51 +36,59 @@ require "packageServer" require "Builder" require "optionparser" -option = parse +begin + option = parse +rescue => e + puts e.message + exit 0 +end #generate server when local package server is not set +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + # if "--os" is not specified, use host os type if option[:os].nil? then option[:os] = Utils::HOST_OS -else - if not option[:os] =~ /^(linux|windows|darwin)$/ then - puts "We have no plan to Buld OS \"#{option[:os]}\" \n please check your option OS " - exit 1 - end end path = Dir.pwd -if not File.exist? "package" then - puts "current dirctory \"#{path}\" is not package root directory" - exit 1 -end +if not File.exist? "package" then + puts "current dirctory \"#{path}\" is not package root directory" + exit 1 +end # if url specified if not option[:url].nil? then begin - builder = Builder.get("default") + builder = Builder.get("default") if builder.pkgserver_url != option[:url] then puts "Package server URL has been changed! Creating new builder..." - builder = Builder.create("default", option[:url], nil) + builder = Builder.create("default", option[:url], nil, nil, nil) end rescue puts "Default builder does not exist! Creating new builder..." - builder = Builder.create("default", option[:url], nil) + builder = Builder.create("default", option[:url], nil, nil, nil) end else # if url is not specified - begin - builder = Builder.get("default") + begin + builder = Builder.get("default") rescue puts "Default builder does not exist! Creating new builder..." - builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil) + builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil, nil, nil) end -end +end #build project -if not builder.build( Utils::WORKING_DIR, option[:os], option[:clean], option[:rev], [], []) then +if not builder.build( Utils::WORKING_DIR, option[:os], option[:clean], [], true) then puts "Build Failed!" + exit 1 else puts "Build Succeeded!" + exit 0 end diff --git a/pkg-clean b/pkg-clean index de48dce..6a6cbb4 100755 --- a/pkg-clean +++ b/pkg-clean @@ -1,7 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby =begin - + pkg-clean Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -37,20 +37,26 @@ require "Builder" require "CleanOptionParser" path = Dir.pwd -if not File.exist? "package" then - puts "current dirctory \"#{path}\" is not package root directory" - exit 1 -end +if not File.exist? "package" then + puts "current dirctory \"#{path}\" is not package root directory" + exit 1 +end option = parse #generate server when local package server is not set -begin - builder = Builder.get("default") +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end + +begin + builder = Builder.get("default") rescue puts "Default builder does not exist! Creating new builder..." - builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil) + builder = Builder.create("default", "http://172.21.111.132/pkgserver/unstable",nil) end #build project diff --git a/pkg-cli b/pkg-cli index 2e0f21e..f2d2e8b 100755 --- a/pkg-cli +++ b/pkg-cli @@ -1,7 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby =begin - + pkg-cli Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -42,137 +42,119 @@ require "packageServer" #set global variable @WORKING_DIR = nil -$log = Logger.new('.log', 'monthly') - -#option parsing +#option parsing begin option = option_parse rescue => e # if option parse error print help message - $log.error "option parsing error" - system "#{__FILE__} help" + puts e.message exit 0 end +# check HOST OS +if not Utils.check_host_OS() then + puts "Error: Your host OS is not supported!" + exit 1 +end #if "--os" is not specfied, use host os type if option[:os].nil? then - system_type = `uname -s` - case system_type.strip - when "Linux" then - option[:os] = "linux" - when /MINGW32.*/ then - option[:os] = "windows" - when "Darwin" then - option[:os] = "darwin" - else - raise RuntimeError, "Unknown OS type : #{system_type}" - end + option[:os] = Utils::HOST_OS end -case option[:cmd] +case option[:cmd] when "update" then - client = Client.new( option[:url], nil, nil ) - client.update() + client = Client.new( option[:url], nil, nil ) + #client.update() when "clean" then - client = Client.new( nil, option[:loc], nil ) - client.clean(option[:f]) + client = Client.new( nil, option[:loc], nil ) + client.clean(option[:f]) when "download" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end + client = Client.new( option[:url], option[:loc], nil ) + #if not option[:url].nil? then + # client.update() + #end file_loc = client.download( option[:pkg], option[:os], option[:t] ) -when "upload" then - client = Client.new( nil, nil, nil ) - result = client.upload( option[:alias], option[:id], option[:binpkg], option[:srcpkg], false ) - if not result.nil? then - puts result - end -when "source" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.download_source( option[:pkg], option[:os] ) when "install" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.install( option[:pkg], option[:os], option[:t], option[:f] ) + client = Client.new( option[:url], option[:loc], nil ) + #if not option[:url].nil? then + # client.update() + #end + client.install( option[:pkg], option[:os], option[:t], option[:f] ) when "install-file" then - client = Client.new( nil, option[:loc], nil ) - client.install_local_pkg( option[:pkg], option[:f] ) + client = Client.new( option[:url], option[:loc], nil ) + client.install_local_pkg( option[:pkg], option[:t], option[:f] ) when "uninstall" then - client = Client.new( nil, option[:loc], nil ) - client.uninstall( option[:pkg], option[:t] ) + client = Client.new( nil, option[:loc], nil ) + client.uninstall( option[:pkg], option[:t] ) when "upgrade" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.upgrade( option[:os], option[:t] ) + client = Client.new( option[:url], option[:loc], nil ) + #if not option[:url].nil? then + # client.update() + #end + client.upgrade( option[:os], option[:t] ) when "check-upgrade" then - client = Client.new( option[:url], option[:loc], nil ) - if not option[:url].nil? then - client.update() - end - client.check_upgrade( option[:os] ) + client = Client.new( option[:url], option[:loc], nil ) + #if not option[:url].nil? then + # client.update() + #end + client.check_upgrade( option[:os] ) when "show-rpkg" then - client = Client.new( option[:url], nil, nil ) - if not option[:url].nil? then - client.update() - end - puts client.show_pkg_info( option[:pkg], option[:os] ) + client = Client.new( option[:url], nil, nil ) + #if not option[:url].nil? then + # client.update() + #end + puts client.show_pkg_info( option[:pkg], option[:os] ) when "list-rpkg" then - client = Client.new( option[:url], nil, nil ) - if not option[:url].nil? then - client.update() - end + client = Client.new( option[:url], nil, nil ) + #if not option[:url].nil? then + # client.update() + #end result = client.show_pkg_list( option[:os] ) - if not result.nil? and not result.empty? then - result.each do |i| - name = i[0].strip - version = i[1].strip - desc = i[2].strip - puts name + " (" + version + ")" - end - end + if not result.nil? and not result.empty? then + result.each do |i| + name = i[0].strip + version = i[1].strip + desc = i[2].strip + puts name + " (" + version + ")" + end + end when "show-lpkg" then - client = Client.new( nil, option[:loc], nil ) - puts client.show_installed_pkg_info( option[:pkg] ) + client = Client.new( nil, option[:loc], nil ) + puts client.show_installed_pkg_info( option[:pkg] ) when "list-lpkg" then - client = Client.new( nil, option[:loc], nil ) + client = Client.new( nil, option[:loc], nil ) result = client.show_installed_pkg_list() - if not result.nil? and not result.empty? then - result.each do |i| - name = i[0].strip - version = i[1].strip - desc = i[2].strip - puts name + " (" + version + ")" - end - end + if not result.nil? and not result.empty? then + result.each do |i| + name = i[0].strip + version = i[1].strip + desc = i[2].strip + puts name + " (" + version + ")" + end + else + puts "Info: There is no any package." + end when "build-dep" then - client = Client.new( nil, nil, nil ) + client = Client.new( nil, nil, nil ) result = client.get_build_dependent_packages( option[:pkg], option[:os], true ) - ret = "" - result.each do |i| - ret = ret + i + " --> " - end - ret = ret.strip - ret[-3..-1] = "" - puts ret + ret = "" + result.each do |i| + ret = ret + i + " --> " + end + ret = ret.strip + ret[-3..-1] = "" + puts ret when "install-dep" then - client = Client.new( nil, nil, nil ) + client = Client.new( nil, nil, nil ) result = client.get_install_dependent_packages( option[:pkg], option[:os], true, false ) - ret = "" - result.each do |i| - ret = ret + i + " --> " - end - ret = ret.strip - ret[-3..-1] = "" - puts ret + ret = "" + result.each do |i| + ret = ret + i + " --> " + end + ret = ret.strip + ret[-3..-1] = "" + puts ret else - raise RuntimeError, "input option incorrect : #{option[:cmd]}" + raise RuntimeError, "Input is incorrect : #{option[:cmd]}" end diff --git a/pkg-svr b/pkg-svr index 3cf4121..6c50176 100755 --- a/pkg-svr +++ b/pkg-svr @@ -1,7 +1,7 @@ -#!/usr/bin/ruby +#!/usr/bin/ruby =begin - + pkg-svr Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -35,67 +35,84 @@ require "utils" require "packageServer" require "serverOptParser" -#option parsing +#option parsing begin option = option_parse rescue => e - puts "\n=============== Error occured ==============================" puts e.message - puts e.backtrace.inspect - puts "=============================================================\n" exit 0 end -begin +begin if option[:cmd].eql? "list" then if option[:id].empty? then - PackageServer.list_id + PackageServer.list_id else PackageServer.list_dist option[:id] - end - exit - end + end + exit + end server = PackageServer.new( option[:id] ) - if server.nil? + if server.nil? raise RuntimeError, "server class creation fail" end - case option[:cmd] - when "create" + case option[:cmd] + when "create" server.create( option[:id], option[:dist], option[:url], option[:loc] ) when "register" - server.register( option[:spkgs], option[:bpkgs], option[:dist], option[:gensnap], option[:test] ) + server.register( option[:pkgs], option[:dist], option[:gensnap], option[:test], false ) when "gen-snapshot" - server.generate_snapshot( option[:snap], option[:dist], option[:bsnap], option[:bpkgs] ) + server.generate_snapshot( option[:snaps][0], option[:dist], option[:bsnap] ) when "sync" - server.sync( option[:dist], option[:force] ) + server.sync( option[:dist], option[:force], option[:snaps][0] ) when "add-dist" server.add_distribution( option[:dist], option[:url], option[:clone] ) - when "spkg-path" - server.find_source_package_path( option[:dist], option[:spkgs] ) + when "add-os" + server.add_os( option[:dist], option[:os] ) when "remove" - puts "Do you want to really? then input \"YES\"" - input = $stdin.gets.strip - if input.eql? "YES" then - puts "Remove server!" - else - puts "Remove is canceled by user input" - exit(0) + if not option[:force] then + puts "Do you want to really? then input \"YES\"" + input = $stdin.gets.strip + if input.eql? "YES" then + puts "Remove server!" + else + puts "Remove is canceled by user input" + exit(0) + end + end + + server.remove_server() + when "remove-dist" + if not option[:force] then + puts "Do you want to really? then input \"YES\"" + input = $stdin.gets.strip + if input.eql? "YES" then + puts "Remove server!" + else + puts "Remove is canceled by user input" + exit(0) + end end - server.remove_server( option[:id] ) + server.remove_dist( option[:dist] ) when "remove-pkg" - server.remove_pkg( option[:id], option[:dist], option[:bpkgs] ) + server.remove_pkg( option[:dist], option[:pkgs], option[:os] ) + when "remove-snapshot" + server.remove_snapshot( option[:dist], option[:snaps] ) + when "clean" + server.clean( option[:dist], option[:snaps] ) + when "start" + server.start( option[:port], option[:passwd] ) + when "stop" + server.stop( option[:port], option[:passwd] ) else raise RuntimeError, "input option incorrect : #{option[:cmd]}" end -rescue => e - puts "\n=============== Error occured ==============================" +rescue => e puts e.message - puts e.backtrace.inspect - puts "=============================================================\n" end diff --git a/src/build_server/BinaryUploadProject.rb b/src/build_server/BinaryUploadProject.rb new file mode 100644 index 0000000..f653b80 --- /dev/null +++ b/src/build_server/BinaryUploadProject.rb @@ -0,0 +1,200 @@ +=begin + + BinaryUploadProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'dbi' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "CommonProject.rb" +require "RegisterPackageJob.rb" +require "Version.rb" +require "PackageManifest.rb" + + +class BinaryUploadProject < CommonProject + attr_accessor :pkg_name + + # initialize + def initialize( name, server, os_list, dist_name, pkg_name = nil ) + super(name, "BINARY", server, os_list, dist_name) + @pkg_name = pkg_name + end + + + # create new job + def create_new_job( filename, dock = "0" ) + file_path = "#{@server.transport_path}/#{dock}/#{filename}" + new_job = create_new_job_from_local_file( file_path ) + if not new_job.nil? then + new_job.set_auto_remove(true) + end + + return new_job + end + + + def create_new_job_from_local_file( file_path ) + filename = File.basename(file_path) + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + pkg_name = new_name.split(",")[0] + os = new_name.split(",")[2] + + # check file name + if @pkg_name != pkg_name then + @server.log.error( "registed name is #{@pkg_name} not #{pkg_name} !", Log::LV_USER) + return nil + end + + # check os name + if not @server.supported_os_list.include? os then + @server.log.error( "server not support OS : #{os}", Log::LV_USER) + return nil + end + + # check package info + if not File.exist? file_path then + @server.log.error( "file not exists in #{file_path}", Log::LV_USER) + return nil + end + + pkginfo_dir = "#{@path}/pkginfos" + if not File.exist? pkginfo_dir then FileUtils.mkdir_p pkginfo_dir end + if not Utils.extract_a_file(file_path, "pkginfo.manifest", pkginfo_dir) then + @server.log.error( "pkginfo.manifest file is not exist", Log::LV_USER) + return nil + end + begin + pkginfo =PackageManifest.new("#{pkginfo_dir}/pkginfo.manifest") + rescue => e + @server.log.error( e.message, Log::LV_USER) + return nil + end + + ##set up change log + #change_log = {} + #begin + # change_log = Parser.read_changelog "#{pkginfo_dir}/changelog" if File.exist? "#{pkginfo_dir}/changelog" + #rescue => e + # @server.log.error( e.message, Log::LV_USER) + # return nil + #end + + #if not change_log.empty? and pkginfo.packages[0].change_log.empty? then + # pkginfo.packages.each {|pkg| pkg.change_log = change_log} + #end + + #if @server.changelog_check and not pkginfo.packages[0].does_change_exist? then + # @server.log.error( "change log not found", Log::LV_USER) + # return nil + #end + + pkgs = pkginfo.get_target_packages(os) + if pkgs.count != 1 then + @server.log.error( "only one package can upload at one time", Log::LV_USER) + return nil + end + if pkgs[0].package_name != @pkg_name then + @server.log.error( "package name is #{pkgs[0].package_name} not #{@pkg_name}", Log::LV_USER) + return nil + end + + new_job = RegisterPackageJob.new( file_path, self, @server, nil, @dist_name ) + end + + + def self.create_table(db, post_fix) + db.do "CREATE TABLE project_bins ( + project_id INTEGER NOT NULL, + pkg_name VARCHAR(32) NOT NULL, + PRIMARY KEY ( project_id ), + CONSTRAINT fk_project_bins_projects1 FOREIGN KEY ( project_id ) REFERENCES projects ( id ) )#{post_fix}" + end + + + def self.load(name, dist_name, server, db) + row, prj_os_list, source_info, package_info = load_row(name, dist_name, db) + prj_id = row['id'] + prj_name = row['name'] + prj_passwd = row['password'] + + new_project = BinaryUploadProject.new(prj_name, server, prj_os_list, dist_name) + if not prj_passwd.empty? then new_project.passwd = prj_passwd end + new_project.set_project_id( prj_id ) + new_project.set_source_info( source_info ) + new_project.set_package_info( package_info ) + + row=db.select_one("SELECT * FROM project_bins WHERE project_id=#{prj_id}") + if row.nil? then return nil end + new_project.pkg_name=row['pkg_name'] + + return new_project + end + + + def save(db) + is_new = save_common(db) + init() + + if is_new then + db.do "INSERT INTO project_bins VALUES (#{@prj_id},'#{@pkg_name}')" + db.do "INSERT INTO group_project_accesses + VALUES ( (SELECT groups.id FROM groups WHERE groups.name = 'admin'),'#{@prj_id}','TRUE')" + else + db.do "UPDATE project_bins SET pkg_name='#{@pkg_name}' WHERE project_id=#{@prj_id})" + end + end + + + def save_source_info(ver,info) + @server.get_db_connection() do |db| + save_source_info_internal(ver,info, db) + end + end + + + # save package info + def save_package_info_from_manifest(version, file_path, os) + begin + pkginfo =PackageManifest.new(file_path) + rescue => e + @server.log.error e.message + return + end + + pkginfo.get_target_packages(os).each do |pkg| + save_package_info(pkg.version, pkg.package_name, os) + end + end + + + def unload(db) + unload_common(db) + if @prj_id != -1 then + db.do "DELETE FROM project_bins WHERE project_id=#{@prj_id}" + end + end +end diff --git a/src/build_server/BuildClientOptionParser.rb b/src/build_server/BuildClientOptionParser.rb index 79941ca..63fb3da 100644 --- a/src/build_server/BuildClientOptionParser.rb +++ b/src/build_server/BuildClientOptionParser.rb @@ -1,6 +1,6 @@ =begin - - BuildClientOptionParser.rb + + BuildClientOptionParser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -26,72 +26,204 @@ Contributors: - S-Core Co., Ltd =end +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" require 'optparse' +require 'utils' + +class BuildClientUsage + BUILD="build-cli build -N -d [-o ] [-w ] [--async] [-D ] [-U user-email] [-V]" + RESOLVE="build-cli resolve -N -d [-o ] [-w ] [--async] [-D ] [-U user-email] [-V]" + QUERY="build-cli query -d " + QUERY_SYSTEM="build-cli query-system -d " + QUERY_PROJECT="build-cli query-project -d " + QUERY_JOB="build-cli query-job -d " + CANCEL="build-cli cancel -j -d [-w ] [-U user-email]" + REGISTER="build-cli register -P -d [-t ] [-w ] [-D ] [-U user-email]" +end + + +def option_error_check( options ) + case options[:cmd] + + when "build" then + if options[:project].nil? or options[:project].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::BUILD + end + + when "resolve" then + if options[:project].nil? or options[:project].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::RESOLVE + end + + when "query" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::QUERY + end + + when "query-system" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::QUERY_SYSTEM + end + + when "query-project" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::QUERY_PROJECT + end + + when "query-job" then + if options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::QUERY_JOB + end -def option_parse - options = {} - banner = "Usage: build-cli {build|resolve|query} ..." + "\n" \ - + "\t" + "build-cli build -g -c [-d ] [-p ] [-o ] [-a ] " + "\n" \ - + "\t" + "build-cli resolve -g -c [-d ] [-p ] [-o ] [-a ] " + "\n" \ - + "\t" + "build-cli query [-d ] [-p ] [-o ] " + "\n" - - optparse = OptionParser.new do|opts| - # Set a banner, displayed at the top - # of the help screen. - - opts.banner = banner - - opts.on( '-g', '--git ', 'git repository' ) do|git| - options[:git] = git - end - - opts.on( '-c', '--commit ', 'git commit id/tag' ) do|git| - options[:commit] = git - end - - options[:domain] = nil - opts.on( '-d', '--domain ', 'remote build server ip address. default 172.21.111.177' ) do|domain| - options[:domain] = domain - end - - options[:port] = nil - opts.on( '-p', '--port ', 'remote build server port. default 2222' ) do|port| - options[:port] = port - end - - options[:os] = nil - opts.on( '-o', '--os ', 'target operating system linux/windows/darwin' ) do|os| - options[:os] = os - end - - options[:async] = "NO" - opts.on( '-a', '--async', 'asynchronous job' ) do - options[:async] = "YES" - end - - opts.on( '-h', '--help', 'display this information' ) do - puts opts + when "cancel" then + if options[:job].nil? or options[:job].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::CANCEL + end + when "register" then + if options[:package].nil? or options[:package].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildClientUsage::REGISTER + end + + else + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" + end + + if ARGV.length > 1 then + raise ArgumentError, "Unknown argument value : #{ARGV[1]}" + end +end + +def option_parse + options = {} + banner = "Requiest service to build-server command-line tool." + "\n" \ + + "\n" + "Usage: build-cli [OPTS] or build-cli (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "build Build and create package." + "\n" \ + + "\t" + "resolve Request change to resolve-status for build-conflict." + "\n" \ + + "\t" + "query Query information about build-server." + "\n" \ + + "\t" + "query-system Query system information about build-server." + "\n" \ + + "\t" + "query-project Query project information about build-server." + "\n" \ + + "\t" + "query-job Query job information about build-server." + "\n" \ + + "\t" + "cancel Cancel a building project." + "\n" \ + + "\t" + "register Register the package to the build-server." + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + BuildClientUsage::BUILD + "\n" \ + + "\t" + BuildClientUsage::RESOLVE + "\n" \ + + "\t" + BuildClientUsage::QUERY + "\n" \ + + "\t" + BuildClientUsage::QUERY_SYSTEM + "\n" \ + + "\t" + BuildClientUsage::QUERY_PROJECT + "\n" \ + + "\t" + BuildClientUsage::QUERY_JOB + "\n" \ + + "\t" + BuildClientUsage::CANCEL + "\n" \ + + "\t" + BuildClientUsage::REGISTER + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + + # Set a banner, displayed at the top + # of the help screen. + + opts.banner = banner + + opts.on( '-N', '--project ', 'project name' ) do|project| + if not Utils.multi_argument_test( project, "," ) then + raise ArgumentError, "Project variable parsing error : #{project}" + end + options[:project] = project + end + + options[:domain] = nil + opts.on( '-d', '--address ', 'build server address: 127.0.0.1:2224' ) do|domain| + options[:domain] = domain + end + + options[:os] = nil + opts.on( '-o', '--os ', 'target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64' ) do |os| + if not Utils.multi_argument_test( os, "," ) then + raise ArgumentError, "OS variable parsing error : #{os}" + end + options[:os] = os + end + + options[:async] = "NO" + opts.on( '--async', 'asynchronous job' ) do + options[:async] = "YES" + end + + options[:noreverse] = "NO" + opts.on( '--noreverse', 'do not check reverse build' ) do + options[:noreverse] = "YES" + end + + opts.on( '-j', '--job ', 'job number' ) do|job| + options[:job] = job + end + + options[:passwd] = "" + opts.on( '-w', '--passwd ', 'password for managing project' ) do|passwd| + options[:passwd] = passwd + end + + opts.on( '-P', '--pkg ', 'package file path' ) do|package| + options[:package] = package.strip + end + + opts.on( '-D', '--dist ', 'distribution name' ) do|dist| + options[:dist] = dist + end + + opts.on( '-t', '--ftp ', 'ftp server url: ftp://dibsftp:dibsftp@127.0.0.1' ) do|domain| + options[:fdomain] = domain + end + + options[:user] = "admin@user" + opts.on( '-U', '--user ', 'user email infomation' ) do|user| + options[:user] = user + end + + options[:verbose] = "NO" + opts.on( '-V', '--verbose', 'verbose mode' ) do + options[:verbose] = "YES" + end + + opts.on( '-h', '--help', 'display help' ) do + opts.help.split("\n").each {|op| puts op if not op.include? "--noreverse"} exit - end - - end - - cmd = ARGV[0] + end - if cmd.eql? "build" or cmd.eql? "resolve" or cmd.eql? "query" or - cmd =~ /(help)|(-h)|(--help)/ then + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end - if cmd.eql? "help" then - ARGV[0] = "-h" + end + + cmd = ARGV[0] + + if cmd.eql? "build" or cmd.eql? "resolve" or + cmd.eql? "query" or cmd.eql? "query-system" or + cmd.eql? "query-project" or cmd.eql? "query-job" or + cmd.eql? "cancel" or + cmd.eql? "register" or + cmd =~ /(-v)|(--version)/ or + cmd =~ /(help)|(-h)|(--help)/ then + + if cmd.eql? "help" then + ARGV[0] = "-h" end - options[:cmd] = ARGV[0] - else - raise ArgumentError, banner - end + options[:cmd] = ARGV[0] + else + raise ArgumentError, "Usage: build-cli [OPTS] or build-cli -h" + end + + optparse.parse! + + option_error_check options - optparse.parse! - - return options -end + return options +end diff --git a/src/build_server/BuildJob.rb b/src/build_server/BuildJob.rb index 2a7f5c7..71c3485 100644 --- a/src/build_server/BuildJob.rb +++ b/src/build_server/BuildJob.rb @@ -1,5 +1,5 @@ =begin - + BuildJob.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -35,89 +35,360 @@ require "client.rb" require "PackageManifest.rb" require "Version.rb" require "Builder.rb" -require "BuildServer.rb" +require "RemoteBuilder.rb" require "JobLog.rb" require "mail.rb" +require "utils.rb" +require "ReverseBuildChecker.rb" +require "CommonJob.rb" -class BuildJob +class BuildJob < CommonJob - attr_accessor :blocked_by + attr_accessor :pkginfo, :source_path + attr_accessor :pkgsvr_client, :thread + attr_accessor :rev_fail_projects, :rev_success_jobs + attr_accessor :pending_ancestor, :cancel_state + attr_accessor :no_reverse + attr_accessor :remote_id # initialize - def initialize () - @blocked_by = [] + def initialize (project, os, server) + super(server) + @project = project + @os = os + @type = "BUILD" + + @cancel_state = "NONE" + @resolve = false + @host_os = Utils::HOST_OS + if not @server.distmgr.nil? then + @pkgsvr_url = @server.distmgr.get_distribution(project.dist_name).pkgsvr_url + @pkgsvr_ip = @server.distmgr.get_distribution(project.dist_name).pkgsvr_ip + @pkgsvr_port = @server.distmgr.get_distribution(project.dist_name).pkgsvr_port + else + @pkgsvr_url = "" + @pkgsvr_ip = "" + @pkgsvr_port = "" + end + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + + # this item will be initialized on pre-verify + @pkginfo = nil + @pkgsvr_client = nil + @thread = nil + @log = nil + @parent = nil # for job hierachy + + #for cancel operation + @pending_ancestor = nil # for cancel pending job + @remote_id = nil # for cancel remote_working job + @build_dep_prjs = nil # for cacnel pending job + + # for resolving build-break + @rev_fail_projects = [] # list of [project,os] + @rev_success_jobs = [] # list of job + + # remote build + @remote_server = nil + + # job type + @is_rev_build_check_job = false + @is_remote_job = false + + # for internal(transferred) job + @is_internal_job = false + @dock_num = "0" + + @external_pkgs = [] + @force_rebuild = false + + @no_reverse = false end - # execute - def execute - @log.info( "Invoking a thread for building Job #{@id}", Log::LV_USER) - if @status == "ERROR" then return end - @thread = Thread.new { - # main - thread_main() - - # close - terminate() - } + + def get_distribution_name() + return @project.dist_name + end + + + def get_buildroot() + return @buildroot_dir + end + + + # set reverse build check job + def set_rev_build_check_job( parent ) + @is_rev_build_check_job = true + + # if parent exists, share build-root + if not parent.nil? then + set_parent_job( parent ) + end + end + + + def is_rev_build_check_job() + return @is_rev_build_check_job + end + + + def set_remote_job(server) + @is_remote_job = true + @remote_server=server + end + + + def get_remote_server() + return @remote_server + end + + + def set_no_reverse() + @no_reverse = true + end + + + def set_internal_job( dock_num ) + @is_internal_job = true + @dock_num = dock_num + end + + + # set option for waiting for resolve + def set_resolve_flag() + @resolve = true + end + + + # set force rebuild + # This make project to build + # even though there is a package of same version on pkg-server + def set_force_rebuild(value) + @force_rebuild = value end - # remote - def execute_remote(server) - @log.info( "Invoking a thread for remote-building Job #{@id}", Log::LV_USER) - if @status == "ERROR" then return end - @thread = Thread.new { - # main - remote_thread_main( server ) - - # close - terminate() - } + # add external packages to overwrite before build + def add_external_package( file_name ) + @external_pkgs.push "#{@job_root}/external_pkgs/#{file_name}" + end + + + #terminate + def terminate() + #do noting + end + + + #cancel + def cancel() + # kill sub process if exist? + kill_sub_process() + + # cancel all its reverse job + @server.jobmgr.reverse_build_jobs.each do |job| + if job.get_parent_job() == self and job.cancel_state == "NONE" then + job.cancel_state = "INIT" + end + end + + # cancel log print + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + + case @status + when "REMOTE_WORKING" then + client = BuildCommClient.create( @remote_server.ip, @remote_server.port, @log ) + if not client.nil? then + client.send "CANCEL|#{@remote_id}|#{self.get_project.passwd}|admin@user" + result1 = client.receive_data() + if result1.nil? then + @log.info( "cancel operation failed [connection error] !!", Log::LV_USER) + else + result1.each do |l| + @log.info(l, Log::LV_USER) + end + end + client.terminate + end + when "PENDING" then + if @pending_ancestor.nil? then + #resolve pending job + pending_descendants = @server.jobmgr.jobs.select do |j| + (not j.pending_ancestor.nil?) and "#{j.pending_ancestor.id}" == "#{@id}" + end + pending_descendants.each do |pd| + pd.cancel_state = "INIT" + end + else + # remove myself from success job if exist + # and add myself into rev_fail_project list if not exist + @pending_ancestor.remove_rev_success_job(self) + @pending_ancestor.add_rev_fail_project( @project, @os ) + + # remove the project that depends on me if exist + # and add it into rev_fail_project list if not exist + p_sub_jobs = @server.jobmgr.jobs.select do |j| + ( not j.pending_ancestor.nil? and + "#{j.pending_ancestor.id}" == "#{@pending_ancestor.id}" and + j.is_build_dependent_project(@project, @os) ) + end + p_sub_jobs.each do |d| + @pending_ancestor.remove_rev_success_job(d) + @pending_ancestor.add_rev_fail_project( d.get_project, d.os ) + + if not d.thread.nil? then d.thread.terminate end + d.status = "WAITING" + end + end + when "WORKING", "WAITING" , "INITIALIZING" , "JUST_CREATED" then + #just log + else # ERROR | FINISHED | RESOLVED + #do noting + end end # check building is possible def can_be_built_on?(host_os) - for pkg in @pkginfo.packages - if pkg.os == @os and pkg.build_host_os.include? host_os then + if @pkginfo.nil? then return false end + + @pkginfo.packages.each do |pkg| + if pkg.os_list.include? @os and pkg.build_host_os.include? host_os then return true end end - return false end + def get_packages() + return @pkginfo.packages + end + + + def get_build_dependencies(target_os) + return @pkginfo.get_build_dependencies(target_os) + end + + + def get_source_dependencies(target_os,host_os) + return @pkginfo.get_source_dependencies(target_os,host_os) + end + + + def is_compatible_with?(o) + # must have same distribution + if get_distribution_name() != o.get_distribution_name() then + return false + end + + if type != o.type then return false end + + my_project = get_project() + other_project = o.get_project() + + # check project name + if my_project.nil? or other_project.nil? or + my_project.name != other_project.name then + return false + end + + # check version + if @pkginfo.nil? or o.pkginfo.nil? or + not(Version.new(@pkginfo.get_version()) == Version.new(o.pkginfo.get_version())) then + return false + end + + # check compat os + @pkginfo.get_target_packages(@os).each do |p| + if not p.os_list.include?(o.os) then return false end + end + + return true + end + + + def has_build_dependency?(other_job) + + if has_same_packages?(other_job) or + does_depend_on?(other_job) or + does_depended_by?(other_job) then + + return true + else + return false + end + end + + def has_same_packages?( wjob ) - for pkg in @pkginfo.packages - for wpkg in wjob.pkginfo.packages + + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + # same package must have same os + if not @os.eql? wjob.os then + return false + end + + # check package name + get_packages().each do |pkg| + wjob.get_packages().each do |wpkg| if pkg.package_name == wpkg.package_name then #puts "Removed from candiated... A == B" return true end end end + return false end def does_depend_on?( wjob ) - for dep in @pkginfo.get_build_dependencies(@os, BuildServer::HOST_OS) - for wpkg in wjob.pkginfo.packages - if dep.package_name == wpkg.package_name then + + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + # compare build dependency + get_build_dependencies(@os).each do |dep| + wjob.get_packages().each do |wpkg| + # dep packages of my job must have same name and target os + # with packages in working job + if dep.package_name == wpkg.package_name and + dep.target_os_list.include? wjob.os then #puts "Removed from candiated... A -> B" return true end end end + return false end def does_depended_by?( wjob ) - for pkg in @pkginfo.packages - for dep in wjob.pkginfo.get_build_dependencies(@os, BuildServer::HOST_OS) - if pkg.package_name == dep.package_name then + + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + get_packages().each do |pkg| + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == pkg.package_name and + dep.target_os_list.include? @os then #puts "Checking... A <- B" return true end @@ -128,89 +399,203 @@ class BuildJob def is_connected? + return @log.is_connected? + end + - # nil? then false - if @outstream.nil? then + # return the job is asyncronous job + def is_asynchronous_job? + if not @log.has_second_out? then + return true + else return false end + end - # send chk signal - begin - BuildCommServer.send_chk( @outstream ) - rescue - return false + + # remove job from reverse success job + def remove_rev_success_job( job ) + @rev_success_jobs.delete job if @rev_success_jobs.include? job + end + + + # check [project,os] is in reverse fail project list + def is_rev_fail_project( prj, os ) + # check the project already exist + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + return true + end end - return true + return false end - # return the job is asyncronous job - def is_asynchronous_job? - if @outstream.nil? then - return true + # add [project,os] to reverse fail project list + def add_rev_fail_project( prj, os ) + # check the project already exist + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + return + end + end + # if not, add it + @rev_fail_projects.push [prj,os] + end + + + # remove [project,os] from reverse fail project list + def remove_rev_fail_project( prj, os ) + remove_list = [] + + # check project and os name + @rev_fail_projects.each do |p| + if p[0] == prj and p[1] == os then + remove_list.push p + end + end + + # remove + remove_list.each do |r| + @rev_fail_projects.delete r + end + end + + + # get project that my job is dependent on + def get_build_dependent_projects() + if @build_dep_prjs.nil? then + deps = @pkginfo.get_build_dependencies(@os) + pkgs = deps.map do |x| + # if "os" is not specified, use my "os" + if x.target_os_list.nil? or x.target_os_list.empty? then + os = @os + else + os = x.target_os_list[0] + end + + # package as item + @pkgsvr_client.get_pkg_from_list(x.package_name, os) + end + prjs = @server.prjmgr.get_projects_from_pkgs(pkgs, get_distribution_name()) + @build_dep_prjs = prjs + end + + return @build_dep_prjs + end + + + # check if the project is my dependent project + def is_build_dependent_project( prj, os ) + dep_list = get_build_dependent_projects() + dep_list.each do |dep| + if dep[0] == prj and dep[1] == os then + return true + end + end + + return false + end + + + def progress + return "" + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + url = "#{@server.job_log_url}/#{@id}/log" + # if remote, the file existence must be checked + if File.exist? "#{@job_root}/remote_log" then + return url,"#{@server.job_log_url}/#{@id}/remote_log" else - return false + return url,"" end end + + # # PROTECTED METHODS # - protected - # main module - def thread_main + # main module + protected + def job_main() + @log.info( "Invoking a thread for building Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end @log.info( "New Job #{@id} is started", Log::LV_USER) - - @status = "BUILDING" - - # update local package server - @server.local_pkgsvr.sync( @server.local_pkgsvr.get_default_dist_name(), false ) - - # checking version - if not check_package_version() - @status = "ERROR" - - return - end # checking build dependency - if not check_build_dependency() + if not @is_remote_job and not @is_internal_job and + not check_build_dependency() then + if @is_internal_job then copy_result_files_to_master() end @status = "ERROR" - return + return false end # clean build - if not build() + if not build() then + if @is_internal_job then copy_result_files_to_master() end + @status = "ERROR" - return + return false end # upload - if not upload() + if not @is_rev_build_check_job and not @is_internal_job and + @parent.nil? and + not upload() then @status = "ERROR" - return + return false end - + + # copy result files to transport path + if @is_internal_job then + copy_result_files_to_master() + elsif not @parent.nil? and not @is_rev_build_check_job then + copy_result_files(@parent.source_path) + end + # INFO. don't change this string @log.info( "Job is completed!", Log::LV_USER) @status = "FINISHED" + return true end # check if local package version is greater than server - def check_package_version() + def check_package_version( source_info ) @log.info( "Checking package version ...", Log::LV_USER) - # package update - @pkgsvr_client.update + # check if version is same and source_info is different + ver_local = @pkginfo.packages[0].version + old_source_info = @project.get_source_info( ver_local ) + if not old_source_info.nil? and old_source_info != source_info then + @log.error( "Source code has been changed without increasing version!", Log::LV_USER) + @log.error( " * Version : #{ver_local}", Log::LV_USER) + @log.error( " * Before : #{old_source_info}", Log::LV_USER) + @log.error( " * Current : #{source_info}", Log::LV_USER) - for pkg in @pkginfo.packages - ver_local = pkg.version - #ver_svr = @pkgsvr_client.get_package_version( pkg.package_name, @os ) - ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") - if not ver_svr.nil? and Version.new(ver_local) <= Version.new(ver_svr) then + return false + end + + # compare with package version in package server + @pkginfo.packages.each do |pkg| + # check all supported os + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + # ignore if package does not exist + if ver_svr.nil? then next end + + # compare version + if Version.new(ver_local) < Version.new(ver_svr) or + ( not @force_rebuild and Version.new(ver_local) == Version.new(ver_svr) ) then @log.error( "Version must be increased : #{ver_local} <= #{ver_svr}", Log::LV_USER) return false end @@ -221,193 +606,592 @@ class BuildJob # build dependency version + # make sure that package server has all dependency packages of job def check_build_dependency() @log.info( "Checking build dependency ...", Log::LV_USER) + @pkgsvr_client.update + unmet_bdeps = [] + @pkginfo.get_build_dependencies( @os ).each do |dep| + # if parent exist, search parent source path first + # if not found, check package server + ver_svr = nil + if not @parent.nil? then + local_pkg = get_local_path_of_dependency( dep, @parent ) + if not local_pkg.nil? then + ver_svr = Utils.get_version_from_package_file( local_pkg ) + else + ver_svr = nil + end + end + if not ver_svr.nil? then next end + + if not remote_package_of_dependency_exist?(dep) then + unmet_bdeps.push dep + end + end + + @log.info( "Checking install dependency ...", Log::LV_USER) + unmet_ideps = [] + @pkginfo.get_install_dependencies( @os ).each do |dep| + # if parent exist, search pkginfos for all sub jobs + # if not found, check package server + found = false + if not @parent.nil? and @parent.type == "MULTIBUILD" then + @parent.get_sub_jobs().each do |j| + os = (dep.target_os_list.empty?) ? @os : dep.target_os_list[0] + if j.pkginfo.pkg_exist?(dep.package_name, dep.base_version, os) then + found = true; break + end + end + end + if found then next end + + if not remote_package_of_dependency_exist?(dep) then + unmet_ideps.push dep + end + end + + # unmet dependencies found , report the errors + if not unmet_bdeps.empty? or not unmet_ideps.empty? then + @log.error( "Unmet dependency found!", Log::LV_USER) + unmet_bdeps.each do |d| + os = (d.target_os_list.empty?) ? @os : d.target_os_list[0] + @log.error( " * #{d.package_name}(#{os}) for build-dependency", Log::LV_USER) + end + unmet_ideps.each do |d| + os = (d.target_os_list.empty?) ? @os : d.target_os_list[0] + @log.error( " * #{d.package_name}(#{os}) for install-dependency", Log::LV_USER) + end + + return false + else + return true + end + end + + + # build clean + def build() - for dep in @pkginfo.get_build_dependencies( @os, @host_os ) - #ver_svr = @pkgsvr_client.get_package_version( dep.package_name, @os ) - if dep.target_os_list.count != 0 then - dep_target_os = dep.target_os_list[0] + # check there are pending packages which wait for me + # it will return nil if not exist + # this process must be skip if it is sub-job + if not @is_rev_build_check_job and not @is_internal_job then + @server.cancel_lock.synchronize do + @pending_ancestor = get_pending_ancestor_job() + end + end + + if not @pending_ancestor.nil? then + # resolve other pending job + resolve() + elsif @resolve then + # wait for being resolved by other jobs + # this condition must be placed after checking pending status + wait_resolve() + else + # build + build_normal() + end + end + + + # return pending job that wait for me + def get_pending_ancestor_job() + @server.jobmgr.get_pending_jobs.each do |job| + # must have same distribution + if get_distribution_name() != job.get_distribution_name() then + next + end + + if job.is_rev_fail_project(@project,@os) then + return job + end + end + + return nil + end + + + # check whether build this job or not + # if not build, then return its compat pkgs list + def check_compatable_packages + compat_pkgs = [] # [ package name, os, local_path ] + + @pkginfo.get_target_packages(@os).each do |p| + # if package has only os then must build + if p.os_list.count <= 1 then return [] end + + compat_found = false + p.os_list.each do |o| + # check parent pkgs first + if not @parent.nil? then + parent_pkgs = Dir.glob("#{@parent.source_path}/#{p.package_name}_*_*.zip") + parent_pkgs.each do |lp| + lpname = Utils.get_package_name_from_package_file( lp ) + lver = Utils.get_version_from_package_file(lp) + los = Utils.get_os_from_package_file( lp ) + if lpname == p.package_name and o == los and lver == p.version then + compat_pkgs.push [p.package_name,o,lp] + compat_found = true + break + end + end + end + if compat_found then break end + + # check other package already in package server + ver_svr = @pkgsvr_client.get_attr_from_pkg( p.package_name, o, "version") + if not ver_svr.nil? and p.version.eql? ver_svr then + compat_pkgs.push [p.package_name,o,nil] + compat_found = true + break + end + end + + # if there is no compat pkgs for one pkg, then must build + if not compat_found then return [] end + end + + return compat_pkgs + end + + + def build_normal() + @log.info( "Started to build this job...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + if not @server.ftp_addr.nil? then + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + end + else + builder = Builder.create( "JB#{@id}", @pkgsvr_url, nil, + "#{@buildroot_dir}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) + return false + end + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgsvr_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) + + # if sub job, install dependent packages of parent-pkgs and not clean + use_clean = true + local_pkgs = [] + local_pkgs += @external_pkgs + if not @parent.nil? then + use_clean = false + # get local packages to install + deps = @pkginfo.get_build_dependencies(@os) + local_pkgs += get_local_paths_of_chained_dependencies( deps, @parent ) + end + local_pkgs.uniq! + + #compatable os support + compat_ok = true + compat_pkgs = check_compatable_packages + if compat_pkgs.size > 0 and not @is_rev_build_check_job then + # bring package from server for reverse check + compat_pkgs.each do |p| + pkg_name = p[0]; cos = p[1]; local_path = p[2] + + if not local_path.nil? then + ext = File.extname(local_path) + base_package_name= File.basename(local_path, "#{cos}#{ext}") + @log.info( "Copying compatible package:#{local_path}", Log::LV_USER) + @log.info( "Creating package file ... #{base_package_name}#{@os}#{ext}", Log::LV_USER) + FileUtils.cp local_path, "#{@source_path}/#{base_package_name}#{@os}#{ext}" + else + @log.info( "Downloading compatible package:#{pkg_name}(#{cos})", Log::LV_USER) + loc = @pkgsvr_client.download(pkg_name, cos, false) + if loc.nil? or loc.count != 1 then + @log.warn( "Downloading compatible package failed!:#{pkg_name}(#{cos})", Log::LV_USER) + compat_ok = false + break + end + ext = File.extname(loc[0]) + base_package_name= File.basename(loc[0], "#{cos}#{ext}") + @log.info( "Creating package file ... #{base_package_name}#{@os}#{ext}", Log::LV_USER) + FileUtils.mv loc[0], "#{@source_path}/#{base_package_name}#{@os}#{ext}" + end + end + else + compat_ok = false + end + + # if compat check failed + if not compat_ok then + # build + if @is_remote_job then + result = builder.build_job(self, local_pkgs) else - dep_target_os = @os + result = builder.build_job(self, use_clean, local_pkgs, false ) end - ver_svr = @pkgsvr_client.get_attr_from_pkg( dep.package_name, dep_target_os, "version") + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() + return false + end + end - if ver_svr.nil? - @log.error( "The package \"#{dep.package_name}\" for build-dependency is not found}", Log::LV_USER) + # check reverse dependecy if not sub jobs + + if not @no_reverse then + if not @is_rev_build_check_job and not @is_internal_job and + not ReverseBuildChecker.check( self, true ).empty? then + @log.error( "Reverse-build-check failed!" ) return false end + end + + return true + end + - if not dep.match? ver_svr - @log.error( "Version for build-dependency in not matched : server version => #{ver_svr}", Log::LV_USER) + # wait to be resolved by other jobs + def wait_resolve() + @log.info( "Started to build this job and wait for being resolved...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + if not @server.ftp_addr.nil? then + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + end + else + builder = Builder.create( "JB#{@id}", @pkgsvr_url, nil, + "#{@buildroot_dir}/#{@os}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) return false end - end - + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgsvr_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) + + # build + if @is_remote_job then + result = builder.build_job(self, []) + else + result = builder.build_job(self, true, [], false ) + end + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() + return false + end + + # check reverse dependecy + @rev_fail_projects = ReverseBuildChecker.check(self, false) + if @rev_fail_projects.empty? then + # if no problem?, it OK + return true + end + + # pending + @status = "PENDING" + @log.info( "Entered the PENDING state ...", Log::LV_USER) + old_msg = "" + while @status == "PENDING" + new_msg = @rev_fail_projects.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + if old_msg != new_msg then + @log.error( " * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end + sleep 1 + end + return true end - # build clean - def build() - if @resolve then - @log.info( "Resolving job...", Log::LV_USER) + # resolve other pending job + def resolve() + + # wait for other build-dependent projects are resolved + old_msg = "" + wait_prjs = @pending_ancestor.rev_fail_projects.select {|p| is_build_dependent_project(p[0], p[1])} + @log.info("Checking build dependency before RESOLVE", Log::LV_USER) + while not wait_prjs.empty? + @status = "PENDING" + new_msg = wait_prjs.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + if new_msg != old_msg then + @log.info(" * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end + sleep 1 + wait_prjs = @pending_ancestor.rev_fail_projects.select {|p| is_build_dependent_project(p[0], p[1])} + end + + # return back to "WORKING" + @status = "WORKING" + + @log.info( "Started to build this job and resolve other pending job...", Log::LV_USER) + + # create builder + if @is_remote_job then + builder = RemoteBuilder.new("JB#{@id}", @remote_server, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Remote Server : #{@remote_server.ip}:#{@remote_server.port}" ) + if not @server.ftp_addr.nil? then + @log.info( " - FTP Server : #{@server.ftp_addr}" ) + end else - @log.info( "Building job...", Log::LV_USER) - end + builder = Builder.create( "JB#{@id}", @pkgsvr_url, nil, + "#{@buildroot_dir}/#{@os}", @server.build_cache_dir ) + if builder.nil? + @log.error( "Creating job builder failed", Log::LV_USER) + return false + end + @log.info( "JobBuilder##{@id} is created", Log::LV_USER) + @log.info( " - Package Server : #{@pkgsvr_url}" ) + @log.info( " - Build Cache Path : #{@server.build_cache_dir}" ) + end + @log.info( " - Log Path : #{@log.path}" ) + + # get local packages to overwite + # they must be composed of packages of pending jobs and its success list + local_pkgs=[] + local_pkgs += @external_pkgs + src_path = @pending_ancestor.source_path + ver = @pending_ancestor.pkginfo.get_version() + @pending_ancestor.pkginfo.get_target_packages(@os).each do |pkg| + local_pkgs.push "#{src_path}/#{pkg.package_name}_#{ver}_#{@os}.zip" + end + @pending_ancestor.rev_success_jobs.each do |job| + src_path = job.source_path + ver = job.pkginfo.get_version() + job.pkginfo.get_target_packages(@os).each do |pkg| + local_pkgs.push "#{src_path}/#{pkg.package_name}_#{ver}_#{@os}.zip" + end + end - # create builder - builder = Builder.create( "JB#{@id}", @pkgserver_url,@log.path ) - if builder.nil? - @log.error( "Creating job builder failed", Log::LV_USER) + # build + if @is_remote_job then + result = builder.build_job(self, local_pkgs) + else + result = builder.build_job(self, true, local_pkgs, false ) + end + if not result then + @log.error( "Building job failed", Log::LV_USER) + write_log_url() return false end - @log.info( "JobBuilder##{@id} is created", Log::LV_USER) - - # set log output - builder.log.close - builder.log = @log - - #make pending_pkg_dir_list - pending_pkg_dir_list = [] - ignore_rev_dep_build_list = [] - @pkginfo.packages.each do |i| - @server.pending_jobs.each do |pj| - if pj.rev_fail_list.include? i.package_name then - pending_pkg_dir_list.push pj.source_path - pending_pkg_dir_list += pj.rev_success_list.map {|pjs| pjs.source_path} - ignore_rev_dep_build_list = pj.rev_fail_list - break - end - end - if not pending_pkg_dir_list.empty? then break end - end - dependency_package_exist = (not pending_pkg_dir_list.empty?) - - # build - if @resolve then - @rev_fail_list = builder.build_resolve(@source_path, @os, [], []) - - # clean build failed - if @rev_fail_list.nil? then - @log.error( "Resolve building job failed", Log::LV_USER) - return false - end - - # pending - @status = "PENDING" - - # rev build successed - if @rev_fail_list.empty? then - @rev_success_list.each do |s| - s.status = "" - end - @status = "" - end - - while @status == "PENDING" - sleep 1 - end - return true - else - if not builder.build(@source_path, @os, true, true, pending_pkg_dir_list, ignore_rev_dep_build_list ) - @log.error( "Building job failed", Log::LV_USER) - return false - end - - if dependency_package_exist then - @server.pending_jobs.each do |j| - if j.source_path == pending_pkg_dir_list[0] then - j.rev_fail_list -= @pkginfo.packages.map{|p| p.package_name} - j.rev_success_list.push self - if j.rev_fail_list.empty? then - j.rev_success_list.each do |s| - s.status = "" - end - j.status = "" - else - @status = "PENDING" - while @status == "PENDING" - sleep 1 - end - end - break - end - end - end - end - - # remove builder - Builder.remove( "builder_#{@id}" ) - + + # check reverse dependecy and update parent rev_fail_project list + new_fail_projects = ReverseBuildChecker.check(self, false) + new_fail_projects.each do |p| + @pending_ancestor.add_rev_fail_project(p[0], p[1]) + end + + # update the status of pending job + @status = "PENDING" + @pending_ancestor.remove_rev_fail_project(@project, @os) + @pending_ancestor.rev_success_jobs.push self + if @pending_ancestor.rev_fail_projects.empty? then + @pending_ancestor.status = "RESOLVED" + @pending_ancestor.rev_success_jobs.each do |job| + job.status = "RESOLVED" + end + else + @log.info( "Entered the PENDING state ...", Log::LV_USER) + old_msg = "" + while @status == "PENDING" + new_msg = @pending_ancestor.rev_fail_projects.map {|p| "#{p[0].name}(#{p[1]})"}.join(", ") + + if new_msg != old_msg then + @log.info(" * Waiting for building next projects: #{new_msg}", Log::LV_USER) + old_msg = new_msg + end + + sleep 1 + end + end + return true end def upload() @log.info( "Uploading ...", Log::LV_USER) - + # get package path list binpkg_path_list = Dir.glob("#{@source_path}/*_*_#{@os}.zip") - srcpkg_path_list = Dir.glob("#{@source_path}/*.tar.gz") # upload - u_client = Client.new( @server.pkgserver_url, nil, @log ) - u_client.update - snapshot = u_client.upload( @server.pkgserver_addr, - @server.pkgserver_id, binpkg_path_list, srcpkg_path_list, true) + u_client = Client.new( @pkgsvr_url, nil, @log ) + snapshot = u_client.upload( @pkgsvr_ip, @pkgsvr_port, binpkg_path_list, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) if snapshot.nil? then @log.info( "Upload failed...", Log::LV_USER) return false end - + # update local @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) @pkgsvr_client.update - @server.local_pkgsvr.sync( @server.local_pkgsvr.get_default_dist_name(), false ) @log.info("Snapshot: #{snapshot}", Log::LV_USER) - + return true end - # remote main module - def remote_thread_main(server) - @log.info( "Job #{@id} is requested to be built on remote server ", Log::LV_USER) - - @status = "REMOTE_BUILDING" + def copy_result_files(dst_path) + @log.info( "Copying result files to #{dst_path}", Log::LV_USER) - # open - client = BuildCommClient.create( server.remote_ip, server.port ) - if client.nil? then - @status = "ERROR" - return - end - - # send & receive - if client.send("BUILD,GIT,#{@git_repos},#{@git_commit},#{@os},,NO") then - result = client.read_lines do |l| - if l.include? "Job is stopped by ERROR" then - @status = "ERROR" - end - # ddd list - @log.output( l.strip, Log::LV_USER) + # get package path list + binpkg_path_list = Dir.glob("#{@source_path}/*_*_#{@os}.zip") + + binpkg_path_list.each do |file| + @log.info( " * #{file}", Log::LV_USER) + FileUtils.cp(file,"#{dst_path}/") + end + + return true + end + + + # copy binary package files and log file to transport dir + def copy_result_files_to_master() + outgoing_dir = "#{@server.transport_path}/#{@dock_num}" + + @log.info( "Copying log to #{outgoing_dir}", Log::LV_USER) + file = "#{@source_path}/../log" + FileUtils.copy_file(file, "#{outgoing_dir}/remote_log") + + # copy result files, if not reverse build + if not @is_rev_build_check_job then + return copy_result_files( outgoing_dir ) + else + return true + end + end + + + protected + def get_local_path_of_dependency( dep, parent ) + dep_target_os = get_os_of_dependency(dep) + + # search my parent job and its parent job + binpkgs = Dir.glob("#{parent.source_path}/#{dep.package_name}_*_#{dep_target_os}.zip") + if binpkgs.count == 0 and not parent.get_parent_job().nil? then + binpkgs = Dir.glob("#{parent.get_parent_job().source_path}/#{dep.package_name}_*_#{dep_target_os}.zip") + end + + if binpkgs.count > 0 then + pkg = binpkgs[0] + version = Utils.get_version_from_package_file(pkg) + if dep.match? version then + return pkg + else + return nil end - if not result then @status = "ERROR" end + else + return nil end + end - # close socket - client.terminate - # INFO. don't change this string - if @status != "ERROR" then - @log.info( "Job is just finished", Log::LV_USER) - @status = "FINISHED" + protected + def get_local_paths_of_chained_dependencies( deps, parent ) + pkg_paths = [] + + # get packages names that is related my build dependency + chained_deps = get_local_chained_dependencies( deps, parent ) + + # get all local path of dependencies + chained_deps.each do |dep| + new_path = get_local_path_of_dependency(dep, parent) + if not new_path.nil? then + pkg_paths.push new_path + end end - return + # remove duplicates + pkg_paths.uniq! + + return pkg_paths + end + + + protected + def get_local_chained_dependencies( deps, parent ) + + chained_deps = [] + chained_deps += deps + + # if parent is multi build job, gether all install dependency of dependency. + if parent.type == "MULTIBUILD" then + begin + old_deps_count = chained_deps.count + new_deps = [] + chained_deps.each do |dep| + dep_target_os = get_os_of_dependency(dep) + + parent.get_sub_jobs().each do |j| + new_deps += j.pkginfo.get_install_dependencies(dep_target_os, dep.package_name) + end + end + chained_deps += new_deps + chained_deps.uniq! {|d| d.package_name } + end while chained_deps.count != old_deps_count + end + + # check parent of parent + if not parent.get_parent_job().nil? then + chained_deps = get_local_chained_dependencies(chained_deps, parent.get_parent_job()) + end + + return chained_deps + end + + + protected + def remote_package_of_dependency_exist?(dep) + dep_target_os = get_os_of_dependency(dep) + + # search + ver_svr = @pkgsvr_client.get_attr_from_pkg( dep.package_name, dep_target_os, "version") + if ver_svr.nil? then return false end + if not dep.match? ver_svr then return false end + + return true + end + + + # write web url for log + protected + def write_log_url() + url,remote_url = get_log_url() + if not url.empty? then + @log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + @log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end + end + + + # get target os of dependency + protected + def get_os_of_dependency(dep) + # use the target os if not specified + if dep.target_os_list.count != 0 then + dep_target_os = dep.target_os_list[0] + else + dep_target_os = @os + end end end diff --git a/src/build_server/BuildServer.rb b/src/build_server/BuildServer.rb index 7862f98..7328abe 100644 --- a/src/build_server/BuildServer.rb +++ b/src/build_server/BuildServer.rb @@ -1,5 +1,5 @@ =begin - + BuildServer.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -26,154 +26,276 @@ Contributors: - S-Core Co., Ltd =end +require 'rubygems' require 'fileutils' +require 'dbi' +require 'thread' $LOAD_PATH.unshift File.dirname(__FILE__) -$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" require "SocketJobRequestListener.rb" -require "RemoteBuildJob.rb" -require "LocalBuildJob.rb" -require "packageServer.rb" +require "JobManager.rb" +require "JobClean.rb" +require "RemoteBuildServer.rb" +require "PackageSync.rb" +require "ProjectManager.rb" +require "DistributionManager.rb" class BuildServer - attr_accessor :id, :path, :pkgserver_url, :pkgserver_addr, :pkgserver_id, :remote_ip, :port, :status, :friend_servers, :host_os, :log - attr_accessor :max_working_jobs, :working_jobs, :waiting_jobs, :remote_jobs, :pending_jobs + attr_accessor :id, :path, :status, :host_os, :log + attr_accessor :remote_servers attr_accessor :git_server_url, :git_bin_path - attr_accessor :job_log_url - attr_accessor :job_index attr_accessor :allowed_git_branch - attr_accessor :pkgsvr_cache_path, :local_pkgsvr - + attr_accessor :jobmgr + attr_accessor :test_time + attr_accessor :password + attr_accessor :finish + attr_accessor :build_cache_dir + attr_accessor :ftp_addr + attr_accessor :ftp_port + attr_accessor :ftp_username + attr_accessor :ftp_passwd + attr_accessor :cleaner + attr_accessor :prjmgr, :distmgr + attr_accessor :transport_path + attr_accessor :cancel_lock + attr_accessor :upgrade + attr_accessor :db + attr_accessor :db_dsn, :db_user, :db_passwd, :db_version CONFIG_ROOT = "#{Utils::HOME}/.build_tools/build_server" HOST_OS = Utils::HOST_OS # initialize - def initialize (id, path, pkgserver_url, pkgserver_addr, pkgserver_id) + def initialize (id, path, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd) @id = id @path = path - @pkgserver_url = pkgserver_url - @pkgserver_addr = pkgserver_addr - @pkgserver_id = pkgserver_id - @friend_servers = [] - @waiting_jobs = [] - @working_jobs = [] - @pending_jobs = [] - @remote_jobs = [] - @max_working_jobs=2 + @remote_servers = [] @req_listener = [] @finish = false - # for friend server - @remote_ip = nil - # port number - @port = 2222 # status @status = "RUNNING" # host_os @host_os = HOST_OS # log - @log = nil + @log =nil @git_server_url = "gerrithost" @git_bin_path = "/usr/bin/git" - @job_index = 0 - @job_log_url = "" @allowed_git_branch = "" # local package server @pkgsvr_cache_path = nil - @local_pkgsvr = nil + # Job Manager + @jobmgr = JobManager.new(self) + @test_time=0 #test time in mili-seconds + @password="0000" + @ftp_addr = ftpsvr_addr + @ftp_port = ftpsvr_port + @ftp_username = ftpsvr_username + @ftp_passwd = ftpsvr_passwd + @cleaner=nil + @prjmgr = ProjectManager.new(self) + @distmgr = DistributionManager.new(self) + # + @transport_path = "#{@path}/transport" + @cancel_lock = Mutex.new + + @upgrade = false + + #DB settring + @db = nil + @db_dsn = "SQLite3:#{BuildServer::CONFIG_ROOT}/#{@id}/server.db" + @db_user = nil + @db_passwd = nil + @db_version = 1 + @sqlite3_db_mutex = Mutex.new + + #DB upgrade SQL command + @db_migrate = [] + #@db_migrate[0]= ["CREATE TABLE test(value INTEGER)", "INSERT INTO test (value) VALUES('3')"] + end + + def send_mail + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'send_mail'")[0] + end + return (result.nil?) ? "NO" : result + end + + def send_mail=(mail) + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{mail}' WHERE property = 'send_mail'" + end + end + + def keep_time + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'keep_time'")[0] + end + return (result.nil?) ? 86400 : result.to_i + end + + def keep_time=(second) + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{second}' WHERE property = 'keep_time'" + end end + def pkg_sync_period + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'pkg_sync_period'")[0] + end + return (result.nil?) ? 600 : result.to_i + end + def pkg_sync_period=(second) + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{second}' WHERE property = 'pkg_sync_period'" + end + end + + def changelog_check + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'changelog_check'")[0] + end + return (result.nil?) ? false : result =~ /true/i + end + + def changelog_check=(check) + t = check =~ /true/i + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{t.to_s}' WHERE property = 'changelog_check'" + end + end + + def job_log_url + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'job_log_url'")[0] + end + return (result.nil?) ? "" : result + end + + def job_log_url=(url) + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{url}' WHERE property = 'job_log_url'" + end + end + + def port + result = nil + get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'port'")[0] + end + return (result.nil?) ? "" : result + end + + def port=(svr_port) + get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{svr_port}' WHERE property = 'port'" + end + end + + def db_inc + return (@db_dsn =~ /^Mysql/i) ? "AUTO_INCREMENT" : "AUTOINCREMENT" + end + + def db_post_fix + return (@db_dsn =~ /^Mysql/i) ? "ENGINE = INNODB DEFAULT CHARSET = UTF8" : "" + end + + def db_now + return (@db_dsn =~ /^Mysql/i) ? "NOW()" : "datetime('now')" + end # start server daemon def start - # start logger - @log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{@id}/log" ) - - # set local package server for cache - @log.info "Setting local package server..." - pkgsvr_id = @id - pkgsvr_dist = @pkgserver_url.split("/")[-1] - @local_pkgsvr = PackageServer.new( pkgsvr_id ) - if @local_pkgsvr.location.empty? then - FileUtils.mkdir_p @pkgsvr_cache_path - @local_pkgsvr.create(pkgsvr_id, pkgsvr_dist, @pkgserver_url, @pkgsvr_cache_path ) - else - # check path is changed, recreate it - if @local_pkgsvr.location != "#{@pkgsvr_cache_path}/#{pkgsvr_id}" then - # remove - @local_pkgsvr.remove_server( pkgsvr_id ) - # create - FileUtils.mkdir_p @pkgsvr_cache_path - @local_pkgsvr.create(pkgsvr_id, pkgsvr_dist, @pkgserver_url, @pkgsvr_cache_path ) - end + # set build cache dir + @build_cache_dir="#{BuildServer::CONFIG_ROOT}/#{@id}/build_cache" + if not File.exist? @build_cache_dir then + FileUtils.mkdir_p @build_cache_dir end + # init transport path + if not File.exist? @transport_path then FileUtils.mkdir_p @transport_path end + + # init project mgr + @log.info "Setting Project Manager..." + @prjmgr.load() + + # init job mgr + @log.info "Intializing Job Manager..." + @jobmgr.init() + # set job request listener @log.info "Setting listener..." listener2 = SocketJobRequestListener.new(self) listener2.start @req_listener.push listener2 - - # main loop - @log.info "Entering main loop..." - while( not @finish ) - # handle jobs - handle() - sleep 1 - end - end + # set job cleaner + @log.info "Setting Job Cleaner..." + @cleaner = JobCleaner.new(self) + @cleaner.start - # stop sever daemon - def stop - @finish = true - end + # set package server synchrontizer + @log.info "Setting Package Server Synchronizer..." + @pkg_sync = PackageServerSynchronizer.new(self) + @pkg_sync.start + # main loop + @log.info "Entering main loop..." + begin + if @test_time > 0 then start_time = Time.now end + while( not @finish ) + + # update friend server status + @remote_servers = get_remote_servers() + @remote_servers.each do |server| + # update state + get_db_connection() do |db| + server.update_state(db) + end + end - # add a normal job - def add_job ( new_job ) - @log.info "Added new job \"#{new_job.id}\"" - - Thread.new { - # pre-verifiy - if not new_job.pre_verify or new_job.status == "ERROR" then - new_job.status = "ERROR" - @log.info "Adding the job \"#{new_job.id}\" is canceled" - new_job.terminate() - Thread.current.exit - end + # handle jobs + @jobmgr.handle() - # check availabiltiy - if not check_job_availability( new_job ) then - new_job.log.error( "No servers that are able to build your packages.", Log::LV_USER) - new_job.status = "ERROR" - @log.info "Adding the job \"#{new_job.id}\" is canceled" - new_job.terminate() - Thread.current.exit + # sleep + if @test_time > 0 then + curr_time = Time.now + if (curr_time - start_time).to_i > @test_time then + puts "Test time is elapsed!" + break + end + else + sleep 1 + end end + rescue => e + @log.error( e.message, Log::LV_USER) + @log.error e.backtrace.inspect + end - @waiting_jobs.push( new_job ) - new_job.status = "WAITING" - @log.info "Checking the job \"#{new_job.id}\" was finished!" - } - end - - - # get job list - def get_job_list() - list = [] - list = list + @working_jobs + @remote_jobs + @waiting_jobs + if(@upgrade) + exit(99) + end + # TODO: something should be done for server down - return list end - # query job status - def get_job_status( job ) - return job.status + # stop sever daemon + def stop + @finish = true end # check the job can be built on this server def can_build?(job) + # check max allowed jobs + if @jobmgr.max_working_jobs <= 0 then + return false + end # check me if job.can_be_built_on? @host_os then @@ -184,294 +306,176 @@ class BuildServer end - def get_new_job_id - # get new id - new_id = @job_index - - # save it - server_dir = "#{BuildServer::CONFIG_ROOT}/#{@id}" - f = File.open( "#{server_dir}/latest_job", "w" ) - f.puts "#{new_id}" - f.close - - # increae job idex - @job_index = @job_index + 1 - - return new_id + def get_remote_servers() + get_db_connection() do |db| + return RemoteBuildServer.load_all(db) + end end # add new remote friend server - def add_friend_server( ip, port ) - - # if already exit, return false - for svr in @friend_servers - if svr.remote_ip.eql? ip and svr.port == port then - return false - end - end - - # create new one, and add it into list - new_server = BuildServer.new( "#{ip}_#{port}", nil, nil, nil, nil ) - new_server.remote_ip = ip - new_server.port = port - new_server.status = "UNDEFINED" - @friend_servers.push new_server + def add_remote_server( ip, port ) + + get_db_connection() do |db| + rs = RemoteBuildServer.load(ip, port, db) + if not rs.nil? then return false end + RemoteBuildServer.new(ip, port, "").save(db) + end return true end - # query remote server info & update server state - def update_state + # remove remote friend server + def remove_remote_server( ip, port ) + get_db_connection() do |db| + rs = RemoteBuildServer.load(ip, port, db) + if rs.nil? then return false end + rs.unload(db) + end - # only friend server - if not @path.nil? then return end + return true + end - # send - @status = "DISCONNECTED" - client = BuildCommClient.create( @remote_ip, @port ) - if client.nil? then return end - if client.send("QUERY,SYSTEM") then - result = client.read_lines do |l| - tok = l.split(",").map { |x| x.strip } - @host_os = tok[0] - @max_working_jobs = tok[1].to_i - @status = "RUNNING" - end - if not result then @status = "DISCONNECTED" end - end - client.terminate - if @status == "DISCONNECTED" then return end - - # send - @working_jobs = [] - @waiting_jobs = [] - client = BuildCommClient.create( @remote_ip, @port ) - if client.nil? then return end - if client.send("QUERY,JOB") then - result = client.read_lines do |l| - tok = l.split(",").map { |x| x.strip } - - job_status = tok[0] - job_id = tok[1] - new_job = RemoteBuildJob.new(job_id) - case job_status - when "WAITING" - @waiting_jobs.push new_job - when "WORKING" - @working_jobs.push new_job - else - #puts "Uncontrolled status" - end + + def get_sync_package_servers() + result = [] + get_db_connection() do |db| + rows = db.select_all("SELECT sync_pkg_servers.pkgsvr_url, distributions.name FROM sync_pkg_servers,distributions WHERE sync_pkg_servers.distribution_id = distributions.id") + rows.each do |row| + result.push [row['pkgsvr_url'], row['name']] end - if not result then @status = "DISCONNECTED" end - else - @status = "DISCONNECTED" end - client.terminate - end + return result + end - private - def handle() - # update friend server status - for server in @friend_servers - # update state - server.update_state + # add new remote pkg server + def add_sync_package_server( url, dist ) + get_db_connection() do |db| + row = db.select_one("SELECT distributions.id FROM sync_pkg_servers,distributions WHERE sync_pkg_servers.pkgsvr_url='#{url}' and distributions.name='#{dist}' and sync_pkg_servers.distribution_id = distributions.id") + if not row.nil? then return false end + row = db.select_one("SELECT id FROM distributions WHERE name='#{dist}'") + dist_id = row['id']; + db.do "INSERT INTO sync_pkg_servers (pkgsvr_url,period,distribution_id) VALUES('#{url}','#{@pkg_sync_period}',#{dist_id})" end - # Move working -> finished - #) Move working -> pending - for job in @working_jobs - if job.status == "ERROR" - @log.info "Job \"#{job.id}\" is stopped by ERROR" - @working_jobs.delete job - elsif job.status == "FINISHED" - @working_jobs.delete job - elsif job.status == "PENDING" - @working_jobs.delete job - @pending_jobs.push job - end - end + return true + end - # Move pending -> finished - for job in @pending_jobs - if job.status == "ERROR" - @log.info "Job \"#{job.id}\" is stopped by ERROR" - @pending_jobs.delete job - elsif job.status == "FINISHED" - @pending_jobs.delete job - end - end - # Move remote -> finished - for job in @remote_jobs - if job.status == "ERROR" - @log.info "Job \"#{job.id}\" is stopped by ERROR" - @remote_jobs.delete job - elsif job.status == "FINISHED" - @remote_jobs.delete job - end + # remove remote pkg server + def remove_sync_package_server( url, dist ) + get_db_connection() do |db| + row = db.select_one("SELECT distributions.id FROM sync_pkg_servers,distributions WHERE sync_pkg_servers.pkgsvr_url='#{url}' and distributions.name='#{dist}' and sync_pkg_servers.distribution_id = distributions.id") + if row.nil? then return false end + dist_id = row['id']; + db.do("DELETE FROM sync_pkg_servers WHERE pkgsvr_url='#{url}' and distribution_id=#{dist_id}") end - # MOVE waiting -> finished - for job in @waiting_jobs - if job.status == "ERROR" then - @waiting_jobs.delete( job ) - @log.info "Job \"#{job.id}\" is removed by internal error" + return true + end + + + def supported_os_list + result = [] + get_db_connection() do |db| + rows = db.select_all("SELECT * FROM supported_os") + rows.each do |row| + result.push row['name'] end end - # MOVE waiting -> working - if @waiting_jobs.count > 0 then - - # get available job - job = get_available_job - - # available job not exist?, continue - if ( job.nil? ) then return end + return result + end - # oherwise, check remote server - rserver = get_available_server( job ) - # request for build - if rserver != nil and rserver == self then + # add new target OS. + # If already exist, return false , otherwise true + def add_supported_os( os_name ) + os_category = Utils.get_os_category( os_name ) + if os_category.nil? then return false end - # change - @waiting_jobs.delete job - @working_jobs.push job + get_db_connection() do |db| + row = db.select_one("SELECT * FROM supported_os WHERE name='#{os_name}'") + if not row.nil? then return false end + db.do "INSERT INTO supported_os(os_category_id, name) SELECT os_category.id, '#{os_name}' FROM os_category WHERE os_category.name='#{os_category}'" + end - # start build - job.execute - @log.info "Moved the job \"#{job.id}\" to working job list" + return true + + end - elsif rserver != nil then - if job.execute_remote( rserver ) then - # status change & job control - job.status = "REMOTE_WAITING" - @waiting_jobs.delete( job ) - @remote_jobs.push( job ) - @log.info "Moved the job \"#{job.id}\" to remote job list" - else - @log.info "Moving the job \"#{job.id}\" to remote failed" - end - else - #puts "No available server" - end + # remove target OS. + def remove_supported_os( os_name ) + get_db_connection() do |db| + row = db.select_one("SELECT * FROM supported_os WHERE name='#{os_name}'") + if row.nil? then return false end + db.do("DELETE FROM supported_os WHERE name='#{os_name}'") end - #check the connection if the waiting job is not asynchronous job - for job in @waiting_jobs - if not job.is_asynchronous_job? and not job.is_connected? then - @waiting_jobs.delete( job ) - @log.info "Job \"#{job.id}\" is disconneted by user. Removed!" - end - end - for job in @remote_jobs - if not job.is_asynchronous_job? and not job.is_connected? then - @remote_jobs.delete( job ) - @log.info "Job \"#{job.id}\" is disconneted by user. Removed!" - end - end - + return true end - # select the job whith no build-dependency problem - def get_available_job - - # gather all working jobs - all_working_jobs = [] + @working_jobs - all_working_jobs = all_working_jobs + @remote_jobs - - # for waiting jobs - for job in @waiting_jobs - blocked_by = [] - is_changed = false - - # should not have same packages with workings - # should not depend on workings - # should not be depended by workings - for wjob in all_working_jobs - if job.has_same_packages?( wjob ) or - job.does_depend_on?( wjob ) or - job.does_depended_by?( wjob ) then - - blocked_by.push wjob - # if there are some changes, check it - if not job.blocked_by.include? wjob then is_changed = true end - end - end - - # if available , then FIFO - if blocked_by.empty? then - job.blocked_by = [] - return job - else - # check count - if blocked_by.count != job.blocked_by.count then is_changed = true end - - # if somthing changed, print it and save it - if is_changed then - job.log.info( "Waiting for finishing following jobs:", Log::LV_USER) - for bjob in blocked_by - job.log.info( " * #{wjob.id} #{wjob.pkginfo.packages[0].source}", Log::LV_USER) - end - job.blocked_by = blocked_by - end - end + def self.get_supported_os_id(db, os_name) + row = db.select_one("SELECT * FROM supported_os WHERE name='#{os_name}'") + if not row.nil? then + return row['id'] + else + return "NULL" end - - return nil end - # get remote server + # get remote server def get_available_server ( job ) candidates = [] - - # check local - if @working_jobs.count < @max_working_jobs and can_build?(job) then + + # calculate empty rooms + # if sub job, his parent should be excluded + local_empty_rooms = @jobmgr.get_number_of_empty_room + + if local_empty_rooms > 0 and can_build?(job) then candidates.push self end - # if Local build job, just check local - if job.instance_of? LocalBuildJob then return candidates[0] end - - # get availables - for server in @friend_servers - # select only "RUNNING" & possible one - if ( server.status == "RUNNING" and server.can_build?( job ) and - server.waiting_jobs.count == 0 and - server.working_jobs.count < server.max_working_jobs ) - candidates.push server + # get availables server + # but, job must not be "REGISTER" and "MULTIBUILD" job + if job.type != "REGISTER" and job.type != "MULTIBUILD" then + @remote_servers.each do |server| + if ( server.status == "RUNNING" and server.can_build?( job ) and + not server.has_waiting_jobs and + server.get_file_transfer_cnt() == 0 and + server.get_number_of_empty_room > 0 ) + candidates.push server + end end end best_server = candidates[0] if best_server.nil? or candidates.count == 1 then return best_server end - # get best + # get best # it is better if working jobs count is less - max_empty_room = best_server.max_working_jobs - best_server.working_jobs.count - for server in candidates - # check whether idle, use it - if server.working_jobs.count == 0 then return server end + max_empty_room = best_server.get_number_of_empty_room + candidates.each do |server| + # check whether idle, use it + if not server.has_working_jobs then return server end # skip if server == best_server then next end - + # compare remain rooms - empty_room = server.max_working_jobs - server.working_jobs.count + empty_room = server.get_number_of_empty_room if empty_room > max_empty_room then - max_empty_room = empty_root + max_empty_room = empty_room best_server = server end end - + return best_server end @@ -483,8 +487,8 @@ class BuildServer if can_build? job then return true end #if not found, check friends - for server in @friend_servers - if server.status == "RUNNING" and + @remote_servers.each do |server| + if server.status == "RUNNING" and job.can_be_built_on? server.host_os then return true end @@ -492,5 +496,271 @@ class BuildServer return false end + + + # return available working slot + def get_number_of_empty_room + return @jobmgr.get_number_of_empty_room + end + + + # check there are working jobs + def has_working_jobs + return @jobmgr.has_working_jobs + end + + + # check there are waiting jobs + def has_waiting_jobs + return @jobmgr.has_waiting_jobs + end + + def get_dbversion + db_version = nil + begin + db = DBI.connect("DBI:#{@db_dsn}", @db_user, @db_passwd) + db_version = db.select_one("SELECT (value) FROM server_configs WHERE property = 'db_version'")[0].to_i + rescue DBI::DatabaseError => e + #puts e.errstr + ensure + db.disconnect if db + end + return db_version + end + + def db_upgrade + result = true + create = false + begin + db = DBI.connect("DBI:#{@db_dsn}", @db_user, @db_passwd) + db_version = db.select_one("SELECT (value) FROM server_configs WHERE property = 'db_version'")[0].to_i + if db_version.nil? then + create = true + else + list = @db_migrate[db_version..@db_version] + if not list.nil? then + list.each do |sql_list| + sql_list.each do |sql| + db.do sql + end + end + end + end + rescue DBI::DatabaseError => e + puts e.errstr + result = false + ensure + db.disconnect if db + end + if create then create_db end + return result + end + + def gen_db() + case @db_dsn + when /^SQLite3:/ then puts "SQLite3 DB#{@db_dsn.split(':')[1]} generating" + when /^Mysql:/ then + name = nil + host = nil + port = nil + socket = nil + flag = nil + dsn = @db_dsn.split(':') + if dsn[2].nil? then + dsn[1].split(';').each do |attr| + case attr.split('=')[0].strip + when /database/i then + name = attr.split('=')[1].strip + when /host/i then + host = attr.split('=')[1].strip + when /port/i then + port = attr.split('=')[1].strip + when /socket/i then + socket = attr.split('=')[1].strip + when /flag/i then + flag = attr.split('=')[1].strip + else + etc = attr.split('=')[1].strip + end + end + else + name = dsn[1].strip + host = dsn[2].strip + end + + File.open("create_db.txt","w") do |f| + f.puts "GRANT ALL ON #{name}.* TO '#{@db_user}'@'%' IDENTIFIED BY '#{@db_passwd}';" + f.puts "CREATE DATABASE #{name};" + end + + if host.eql? "localhost" or host.eql? "127.0.0.1" then + socket_str = "" + socket_str = "--socket=#{socket}" if not socket.nil? + puts "Mysql DB #{name} generating" + system("mysql -h #{host} #{socket_str} -u #{@db_user} --password=#{@db_passwd} < create_db.txt") + else + port_str = "" + port_str = "-P #{port}" if not port.nil? + puts "Mysql DB #{name} generating" + system("mysql -h #{host} #{port_str} -u #{@db_user} --password=#{@db_passwd} < create_db.txt") + end + else puts "not support DB #{@db_dsn}" + end + end + + def create_db() + gen_db() + result = get_db_connection() do |db| + inc = db_inc + post_fix = db_post_fix + now = db_now + + # remove table + db.tables.each do |table| + db.do "DROP TABLE #{table}" + end + + # create table + db.do "CREATE TABLE server_configs ( id INTEGER PRIMARY KEY #{inc}, property VARCHAR(64) NOT NULL, value VARCHAR(256) )#{post_fix}" + db.do "INSERT INTO server_configs (property,value) VALUES ('id','#{@id}')" + db.do "INSERT INTO server_configs (property,value) VALUES ('path','#{@path}')" + db.do "INSERT INTO server_configs (property,value) VALUES ('db_version','#{@db_version}')" + db.do "INSERT INTO server_configs (property,value) VALUES ('port','2222')" + db.do "INSERT INTO server_configs (property,value) VALUES ('max_working_jobs','2')" + db.do "INSERT INTO server_configs (property,value) VALUES ('send_mail','NO')" + db.do "INSERT INTO server_configs (property,value) VALUES ('keep_time','86400')" + db.do "INSERT INTO server_configs (property,value) VALUES ('pkg_sync_period','600')" + db.do "INSERT INTO server_configs (property,value) VALUES ('changelog_check','false')" + db.do "INSERT INTO server_configs (property,value) VALUES ('job_log_url','')" + db.do "CREATE TABLE os_category ( id INTEGER PRIMARY KEY #{inc}, name VARCHAR(32) NOT NULL UNIQUE ) #{post_fix}" + db.do "INSERT INTO os_category (name) VALUES ( 'linux' )" + db.do "INSERT INTO os_category (name) VALUES ( 'windows' )" + db.do "INSERT INTO os_category (name) VALUES ( 'macos' )" + + PackageDistribution.create_table(db, inc, post_fix) + + # sync package server + db.do "CREATE TABLE sync_pkg_servers ( + id INTEGER PRIMARY KEY #{inc}, + distribution_id INTEGER NOT NULL, + pkgsvr_url VARCHAR(256), + period INTEGER, + description VARCHAR(255), + CONSTRAINT fk_sync_pkg_servers_distributions1 FOREIGN KEY ( distribution_id ) REFERENCES distributions ( id ) ) #{post_fix}" + + db.do "CREATE TABLE supported_os ( + id INTEGER PRIMARY KEY #{inc}, + os_category_id INTEGER NOT NULL, + name VARCHAR(32) NOT NULL UNIQUE, + CONSTRAINT fk_supported_os_os_category1 FOREIGN KEY ( os_category_id ) REFERENCES os_category ( id ) ) #{post_fix}" + + RemoteBuildServer.create_table(db, inc, post_fix) + + # USERS/GROUPS + # users + db.do "CREATE TABLE users ( id INTEGER PRIMARY KEY #{inc}, name VARCHAR(32) NOT NULL UNIQUE, email VARCHAR(256), password_hash VARCHAR(256), password_salt VARCHAR(256) ) #{post_fix}" + # groups + db.do "CREATE TABLE groups ( id INTEGER PRIMARY KEY #{inc}, name VARCHAR(32) NOT NULL UNIQUE, admin VARCHAR(32) NOT NULL DEFAULT 'FALSE', description VARCHAR(256) )#{post_fix}" + # user groups (users -- groups) + db.do "CREATE TABLE user_groups ( user_id INTEGER NOT NULL, group_id INTEGER NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE', PRIMARY KEY ( user_id, group_id ), + CONSTRAINT fk_users_has_groups_users1 FOREIGN KEY ( user_id ) REFERENCES users ( id ), + CONSTRAINT fk_users_has_groups_groups1 FOREIGN KEY ( group_id ) REFERENCES groups ( id )) #{post_fix}" + + db.do "INSERT INTO users (name,email,password_hash,password_salt) VALUES ('administrators','admin@user','$2a$10$H.w3ssI9KfuvNEXXp1qxD.b3Wm8alJG.HXviUofe4nErDn.TKUAka','$2a$10$H.w3ssI9KfuvNEXXp1qxD.')" + db.do "INSERT INTO groups (name, admin, description) VALUES ('admin','TRUE','')" + db.do "INSERT INTO user_groups (user_id, group_id) SELECT users.id,groups.id FROM users,groups WHERE users.email = 'admin@user' and groups.name = 'admin'" + + # PROJECTS + CommonProject.create_table(db, inc, post_fix) + GitBuildProject.create_table(db, post_fix) + BinaryUploadProject.create_table(db, post_fix) + db.do "CREATE TABLE group_project_accesses( + group_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + build VARCHAR(32) NOT NULL DEFAULT 'TRUE', + PRIMARY KEY ( group_id,project_id ), + CONSTRAINT fk_groups_has_projects_groups1 FOREIGN KEY ( group_id ) REFERENCES groups ( id ), + CONSTRAINT fk_groups_has_projects_projects1 FOREIGN KEY ( project_id ) REFERENCES projects ( id ) )#{post_fix}" + + # JOBS + CommonJob.create_table(db, post_fix) + end + + return result + end + + + def get_db_connection(transaction=true) + begin + if @db_dsn =~ /^SQLite3:/ then + @sqlite3_db_mutex.lock + @db = DBI.connect("DBI:#{@db_dsn}", @db_user, @db_passwd) + else + if @db.nil? or not @db.connected? then + @db = DBI.connect("DBI:#{@db_dsn}", @db_user, @db_passwd) + end + end + if transaction then + @db['AutoCommit'] = false + begin + @db.transaction do |dbh| + yield dbh if block_given? + end + ensure + @db['AutoCommit'] = true + end + else + yield @db if block_given? + end + + return true + + rescue DBI::DatabaseError => e + @log.error "DB loading failed!" if not @log.nil? + @log.error e.errstr if not @log.nil? + @log.error e.backtrace.inspect if not @log.nil? + + ensure + if @db_dsn =~ /^SQLite3:/ then + @db.disconnect if @db + @db = nil + @sqlite3_db_mutex.unlock + end + end + + return false + end + + + def check_user_id_from_email(user_email) + get_db_connection() do |db| + row = db.select_one("SELECT * FROM users WHERE email='#{user_email}'") + if not row.nil? then + return row['id'] + else + return -1 + end + end + + return -1 + end + + + def get_email_using_user_id(user_id) + get_db_connection() do |db| + row = db.select_one("SELECT * FROM users WHERE id='#{user_id}'") + if not row.nil? then + return row['email'] + else + return -1 + end + end + return -1 + end + + + def qualify_admin_to_access(prj_id) + # nothing to do, admin can change everything on web admin tool + end end diff --git a/src/build_server/BuildServerController.rb b/src/build_server/BuildServerController.rb index b0b237b..eabded1 100644 --- a/src/build_server/BuildServerController.rb +++ b/src/build_server/BuildServerController.rb @@ -1,5 +1,5 @@ =begin - + BuildServerController.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -34,7 +34,7 @@ class BuildServerController @@instance_map = {} # create - def self.create_server (id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id) + def self.create_server (id, path, ftpsvr_addr=nil, ftpsvr_port=nil, ftpsvr_username=nil, ftpsvr_passwd=nil) # check server config root check_build_server_root @@ -45,24 +45,20 @@ class BuildServerController end # create new instance and return it - @@instance_map[id] = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id ) - + @@instance_map[id] = BuildServer.new( id, path, ftpsvr_addr, ftpsvr_port, ftpsvr_username, ftpsvr_passwd) # set default - @@instance_map[id].git_server_url="gerrithost:" - if Utils::HOST_OS == "windows" then + if Utils.is_windows_like_os(Utils::HOST_OS) then @@instance_map[id].git_bin_path="/c/Program\\ Files/Git/bin/git.exe" else @@instance_map[id].git_bin_path="/usr/bin/git" end - @@instance_map[id].allowed_git_branch=nil - @@instance_map[id].max_working_jobs= 2 - @@instance_map[id].job_log_url="" - @@instance_map[id].pkgsvr_cache_path="#{path}/pkgsvr_cache" - # write config write_server_config( @@instance_map[id] ) + # set logger + @@instance_map[id].log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{id}/log" ) + puts "Created new build server: \"#{id}\"" return @@instance_map[id] end @@ -78,31 +74,49 @@ class BuildServerController FileUtils.rm_rf "#{BuildServer::CONFIG_ROOT}/#{id}" @@instance_map[id] = nil puts "Removed the server \"#{id}\"" + else + puts "The server \"#{id}\" does not exist!" end - - # remove local package server - local_pkgsvr = PackageServer.new( id ) - local_pkgsvr.remove_server(id) + end + def self.migrate_server (id, dsn, user, passwd) + # set DB environment + server = get_server(id) + server.db_dsn = dsn if not dsn.nil? + server.db_user = user if not user.nil? + server.db_passwd = passwd if not passwd.nil? + + # check db + if migrate_db(server) then + write_server_config(server) + end end + def self.migrate_db (server) + version = server.get_dbversion + if version.nil? then server.create_db end + return server.db_upgrade + end # get def self.get_server( id ) # check instance first if not @@instance_map[id] == nil - return @@instance_map[id] + return @@instance_map[id] end - # check server config - if not File.exist? "#{BuildServer::CONFIG_ROOT}/#{id}/server.cfg" - raise RuntimeError, "The server \"#{id}\" does not exist." + # check server config + if not File.exist? "#{BuildServer::CONFIG_ROOT}/#{id}/server.cfg" + raise RuntimeError, "The server \"#{id}\" does not exist!" end # get server config and return its object @@instance_map[id] = read_server_config(id) + # set logger first + @@instance_map[id].log = Log.new( "#{BuildServer::CONFIG_ROOT}/#{id}/log" ) + return @@instance_map[id] end @@ -110,24 +124,14 @@ class BuildServerController # start server def self.start_server( id, port = 2222 ) server = get_server(id) - - # write run port + migrate_db(server) + server.jobmgr.cancel_broken_status + + # write run port server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" f = File.open( "#{server_dir}/run", "w" ) f.puts port - f.close - - # write run job - if File.exist? "#{server_dir}/latest_job" then - f = File.open( "#{server_dir}/latest_job", "r" ) - server.job_index = f.gets.strip.to_i + 1 - f.close - else - f = File.open( "#{server_dir}/latest_job", "w" ) - f.puts "0" - server.job_index = 0 - f.close - end + f.close # start server.port = port @@ -137,29 +141,141 @@ class BuildServerController # stop server def self.stop_server( id ) + + # server + server = get_server(id) + migrate_db(server) + client = BuildCommClient.create( "127.0.0.1", server.port ) + if client.nil? then + puts "Server is not running!" + return false + end + + # send request + stop_ok = false + if client.send "STOP|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + stop_ok = true + end + end + + # terminate + client.terminate + + if not stop_ok then + puts "Server stop failed!" + end + + return true + end + + # upgrade server + def self.upgrade_server( id ) + + # server + server = get_server(id) + migrate_db(server) + client = BuildCommClient.create( "127.0.0.1", server.port ) + if client.nil? then + puts "Server is not running!" + return false + end + + # send request + upgrade_ok = false + if client.send "UPGRADE|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + upgrade_ok = true + end + end + + # terminate + client.terminate + + if not upgrade_ok then + puts "Server upgrade failed!" + end + + return true + end + + # request upgrade friends build server + def self.request_upgrade_server( id ) + server = get_server(id) + migrate_db(server) + server_dir = "#{BuildServer::CONFIG_ROOT}/#{id}" + + if File.exist? "#{server_dir}/friends" then + File.open( "#{server_dir}/friends", "r" ) do |f| + f.each_line do |l| + if l.split(",").count < 2 then next end + ip = l.split(",")[0].strip + port = l.split(",")[1].strip - # if the server is not running => error + client = BuildCommClient.create( ip, port ) + if client.nil? then + puts "Friend Server #{ip}:#{port} is not running!" + next + end + # send request + upgrade_ok = false + if client.send "UPGRADE|#{server.password}" then + # recevie & print + mismatched = false + result = client.read_lines do |l2| + puts l2 + if l2.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + upgrade_ok = true + end + end + + # terminate + client.terminate + + if upgrade_ok then + puts "Friend Server #{ip}:#{port} upgrade requested!" + else + puts "Friend Server #{ip}:#{port} upgrade failed!" + end + end + end + else + puts "No Friend Server." + end - # request stop + return true end # add friend server - def self.add_friend_server( id, ip, port ) + def self.add_remote_server( id, ip, port ) server = get_server(id) - + migrate_db(server) + # add - if server.add_friend_server( ip, port ) then - - # write config - server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" - f = File.open( "#{server_dir}/friends", "a" ) - f.puts "#{ip},#{port}" - f.close - + if server.add_remote_server( ip, port ) then puts "Friend server is added successfully!" - return true else puts "Friend server already exists in list!" @@ -168,87 +284,538 @@ class BuildServerController end - # build git repository and upload - def self.build_git( id, repository, commit, os, url, resolve ) + # remove friend server + def self.remove_remote_server( id, ip, port ) + server = get_server(id) + migrate_db(server) - # server + # add + if server.remove_remote_server( ip, port ) then + puts "Friend server is removed successfully!" + return true + else + puts "Friend server does not exist in list!" + return false + end + end + + + # add supported target os + def self.add_target_os( id, os_name ) + # get server server = get_server(id) - client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + migrate_db(server) - # send request - client.send "BUILD,GIT,#{repository},#{commit},#{os}" + # add + if server.add_supported_os( os_name ) then + puts "Target OS is added successfully!" + return true + else + puts "Target OS already exists in list!" + return false + end + end - # recevie & print - client.print_stream - # terminate - client.terminate + # remove supported target os + def self.remove_target_os( id, os_name ) + # get server + server = get_server(id) + migrate_db(server) - return true + # add + if server.remove_supported_os( os_name ) then + puts "Target OS is removed successfully!" + return true + else + puts "Target OS does not exist in list!" + return false + end + + server.quit end - # resolve git and build it and upload - def resolve_git( id, repository, commit, os, url ) - # server + # add distribution + def self.add_distribution( id, dist_name, pkgsvr_url, pkgsvr_ip, pkgsvr_port ) + # get server server = get_server(id) - client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + migrate_db(server) - # send request - client.send "RESOLVE,GIT,#{repository},#{commit},#{os}" + # add + if server.distmgr.add_distribution( dist_name, pkgsvr_url, pkgsvr_ip, pkgsvr_port ) then + puts "Distribution is added successfully!" + return true + else + puts "Distribution already exists in list!" + return false + end + end - # recevie & print - client.print_stream - # terminate - client.terminate + # remove distribution + def self.remove_distribution( id, dist_name ) + # get server + server = get_server(id) + migrate_db(server) - return true + # remove + if server.distmgr.remove_distribution( dist_name ) then + puts "Distribution is removed successfully!" + return true + else + puts "Distribution does not exist in list!" + return false + end + end + + + # lock distribution + def self.lock_distribution(id, dist_name) + # get server + server = get_server(id) + migrate_db(server) + + # remove + if server.distmgr.set_distribution_lock(dist_name, true) then + puts "Distribution is locked!" + return true + else + puts "Locking distribution failed!" + return false + end + end + + + # unlock distribution + def self.unlock_distribution(id, dist_name) + # get server + server = get_server(id) + migrate_db(server) + + # remove + if server.distmgr.set_distribution_lock(dist_name, false) then + puts "Distribution is unlocked!" + return true + else + puts "Unlocking distribution failed!" + return false + end + end + + + # add remote package server + def self.add_sync_package_server(id, url, dist_name) + server = get_server(id) + migrate_db(server) + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + # add + if server.add_sync_package_server( url, dist_name ) then + puts "Remote package server is added!" + return true + else + puts "The server already exists in list!" + return false + end + end + + + def self.check_distribution_name(dist_name, server) + if (dist_name.nil? or dist_name.empty?) then + dist_name = server.distmgr.get_default_distribution_name() + end + if dist_name.nil? or dist_name.empty? then + puts "No distribution is defined!" + return nil + end + + return dist_name + end + + # remove remote package server + def self.remove_sync_package_server(id, url, dist_name) + server = get_server(id) + migrate_db(server) + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + # remove + if server.remove_sync_package_server( url, dist_name ) then + puts "Remote package server is removed!" + return true + else + puts "The server does not exist in list!" + return false + end + end + + + # add project + def self.add_project( id, project_name, git_repos, git_branch, remote_server_id, + passwd, os_string, dist_name ) + # get server + server = get_server(id) + migrate_db(server) + + # get supported os for project. + # if not specified, all supported os of the server will be used + if os_string.nil? or os_string.empty? then + os_list = server.supported_os_list + else + os_list = os_string.strip.split(",") + + # check OS name + os_list.each do |os| + if not server.supported_os_list.include? os then + puts "Unsupported OS name \"#{os}\" is used!" + puts "Check the following supported OS list:" + server.supported_os_list.each do |s_os| + puts " * #{s_os}" + end + + return false + end + end + end + + # add + if git_repos.nil? or git_branch.nil? then + puts "Git repository or branch must be specified!" + return false + end + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + result = server.prjmgr.add_git_project( project_name, git_repos, git_branch, passwd, os_list, dist_name ) + if result then + puts "Adding project succeeded!" + return true + else + puts "Adding project failed!" + return false + end + end + + + # add binary project + def self.add_binary_project( id, project_name, pkg_name, passwd, os_string, dist_name ) + # get server + server = get_server(id) + migrate_db(server) + + # get supported os for project. + # if not specified, all supported os of the server will be used + if os_string.nil? or os_string == "default" then + os_list = server.supported_os_list + else + os_list = os_string.strip.split(",") + # check OS name + os_list.each do |os| + if not server.supported_os_list.include? os then + puts "Unsupported OS name \"#{os}\" is used!" + puts "Check the following supported OS list:" + server.supported_os_list.each do |s_os| + puts " * #{s_os}" + end + + return false + end + end + end + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + # add + result = server.prjmgr.add_binary_project( project_name, pkg_name, passwd, + os_list, dist_name ) + if result then + puts "Adding project succeeded!" + return true + else + puts "Adding project failed!" + return false + end + end + + + # remove project + def self.remove_project( id, project_name, dist_name ) + # get server + server = get_server(id) + migrate_db(server) + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + result = server.prjmgr.remove_project( project_name, dist_name ) + if result then + puts "Removing project succeeded!" + return true + else + puts "Removing project failed!" + return false + end end - # build local project and upload - def self.build_local( id, local_path, os, url, resolve ) + # full build + def self.build_all_projects( id, dist_name ) + # server server = get_server(id) + migrate_db(server) + + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + if client.nil? then + puts "Server is not running!" + return false + end # send request - client.send "BUILD,LOCAL,#{local_path},#{os}" - - # recevie & print - client.print_stream + fullbuild_ok = false + if client.send "FULLBUILD|#{server.password}|#{dist_name}" then + # recevie & print + mismatched = false + dist_not_found = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + if l.include? "Distribution not found!" then + dist_not_found = true + end + end + if result and not mismatched and not dist_not_found then + fullbuild_ok = true + end + end - # terminate + # terminate client.terminate + if not fullbuild_ok then + puts "Full build failed!" + end + return true end - # resolve local project and build it and upload - def resolve_local( path, os ) + def self.register_package(id, file_path, dist_name) # server server = get_server(id) - client = BuildCommClient.create( "127.0.0.1", server.port ) - if client.nil? then return false end + migrate_db(server) - # send request - client.send "RESOLVE,LOCAL,#{local_path},#{os}" + # check distribution + dist_name = check_distribution_name(dist_name, server) + if dist_name.nil? then return false end + + # check file exist? + if not File.exist? file_path then + puts "File not found!" + return false + end + file_path = File.expand_path(file_path) - # recevie & print - client.print_stream + # send request + success = false + client = BuildCommClient.create( "127.0.0.1", server.port ) + if client.nil? then + puts "Server is not running!" + return false + end + if client.send "REGISTER|BINARY-LOCAL|#{file_path}|#{server.password}|#{dist_name}" then + # recevie & print + mismatched = false + result = client.read_lines do |l| + puts l + if l.include? "Password mismatched!" then + mismatched = true + end + end + if result and not mismatched then + success = true + end + end - # terminate + # terminate client.terminate + if not success then + puts "Registering package failed!" + end + return true end + # server + def self.query_server( id ) + server = get_server(id) + migrate_db(server) + server.prjmgr.load() + + puts "* REMOTE SERVER(S) *" + server.get_remote_servers.each do |s| + puts " * #{s.ip}:#{s.port}" + end + puts "" + + puts "* SUPPORTED OS *" + server.supported_os_list.each do |os| + puts " * #{os}" + end + puts "" + + puts "* DISTRIBUTION(S) *" + server.distmgr.get_all_distributions().each do |d| + puts " * #{d.name}" + end + puts "" + + puts "* SYNC PACKAGE SERVER(S) *" + server.get_sync_package_servers.each do |s| + puts " * [#{s[1]}] #{s[0]}" + end + puts "" + + puts "* PROJECT(S) *" + server.prjmgr.get_all_projects().each do |p| + puts " * [#{p.dist_name}] #{p.name}" + end + end + + + # server + def self.set_server_attribute( id, attr, value ) + result = true + + if attr.nil? or attr.empty? or value.nil? or value.empty? then + puts "Attribute/Value must be specified!" + return false + end + + server = get_server(id) + case attr + when "GIT_BIN_PATH" + server.git_bin_path = value + when "MAX_WORKING_JOBS" + server.jobmgr.max_working_jobs = value.to_i + when "JOB_LOG_URL" + server.job_log_url = value + when "SEND_MAIL" + server.send_mail = value + when "TEST_TIME" + server.test_time = value.to_i + when "PASSWORD" + server.password = value + when "JOB_KEEP_TIME" + server.keep_time = value.to_i + when "FTP_ADDR" + server.ftp_addr = value + when "FTP_PORT" + server.ftp_port = value + when "FTP_USERNAME" + server.ftp_username = value + when "FTP_PASSWD" + server.ftp_passwd = value + when "CHANGELOG_CHECK" + server.changelog_check = value + when "DB_DSN" + case value + when /^SQLite3:(.*)/i then + if $1.strip.empty? then db_dsn = "SQLite3:#{BuildServer::CONFIG_ROOT}/#{id}/server.db" + else db_dsn = "SQLite3:#{$1.strip}" + end + when /^Mysql:(.*)/i then + db_dsn = "Mysql:#{$1}" + else + db_dsn = "SQLite3:#{BuildServer::CONFIG_ROOT}/#{id}/server.db" + end + server.db_dsn = db_dsn + when "DB_USER" + server.db_user = value if not value.empty? + when "DB_PASSWORD" + server.db_passwd = value if not value.empty? + else + puts "Wrong attribute name!" + result = false + end + + if result then write_server_config( server ) end + + return result + end + + + # server + def self.get_server_attribute( id, attr ) + result = true + + if attr.nil? or attr.empty? then + puts "Attribute name must be specified!" + return false + end + + server = get_server(id) + case attr + when "GIT_BIN_PATH" + puts server.git_bin_path + when "MAX_WORKING_JOBS" + puts server.jobmgr.max_working_jobs + when "JOB_LOG_URL" + puts server.job_log_url + when "SEND_MAIL" + puts server.send_mail + when "TEST_TIME" + puts server.test_time + when "PASSWORD" + puts server.password + when "JOB_KEEP_TIME" + puts server.keep_time + when "FTP_ADDR" + puts server.ftp_addr + when "FTP_PORT" + puts server.ftp_port + when "FTP_USERNAME" + puts server.ftp_username + when "FTP_PASSWD" + puts server.ftp_passwd + when "CHANGELOG_CHECK" + puts server.changelog_check + when "DB_DSN" + puts server.db_dsn + when "DB_USER" + puts server.db_user + when "DB_PASSWORD" + puts server.db_passwd + else + puts "Wrong attribute name!" + result = false + end + + return result + end + + + ##################### private + ##################### # check build server config path def self.check_build_server_root @@ -264,21 +831,24 @@ class BuildServerController def self.write_server_config( server ) # create config folder server_dir = "#{BuildServer::CONFIG_ROOT}/#{server.id}" - FileUtils.mkdir_p( "#{server_dir}" ) + if not File.exist? server_dir then + FileUtils.mkdir_p( server_dir ) + end # write configuration File.open( "#{server_dir}/server.cfg", "w" ) do |f| f.puts "ID=#{server.id}" f.puts "PATH=#{server.path}" - f.puts "PSERVER_URL=#{server.pkgserver_url}" - f.puts "PSERVER_ADDR=#{server.pkgserver_addr}" - f.puts "PSERVER_ID=#{server.pkgserver_id}" - f.puts "PSERVER_CACHE_PATH=#{server.pkgsvr_cache_path}" - f.puts "GIT_SERVER_URL=#{server.git_server_url}" f.puts "GIT_BIN_PATH=#{server.git_bin_path}" - f.puts "ALLOWED_GIT_BRANCH=#{server.allowed_git_branch}" - f.puts "MAX_WORKING_JOBS=#{server.max_working_jobs}" - f.puts "JOB_LOG_URL=#{server.job_log_url}" + f.puts "TEST_TIME=#{server.test_time}" if server.test_time > 0 + f.puts "PASSWORD=#{server.test_time}" if server.password != "0000" + if not server.ftp_addr.nil? then + f.puts "FTP_URL=ftp://#{server.ftp_username}:#{server.ftp_passwd}@#{server.ftp_addr}:#{server.ftp_port}" + end + f.puts "#only supports \"Mysql\" and \"SQLite3\"" + f.puts "DB_DSN=#{server.db_dsn}" + f.puts "DB_USER=#{server.db_user}" + f.puts "DB_PASSWORD=#{server.db_passwd}" end end @@ -286,48 +856,62 @@ class BuildServerController # read server configuration def self.read_server_config( id ) path="" - pkgsvr_url="" - pkgsvr_addr="" - pkgsvr_id="" - pkgsvr_cache_path="" - git_server_url="gerrithost:" git_bin_path="/usr/bin/git" - allowed_git_branch="" - max_working_jobs= 2 - job_log_url="" + test_time=0 + password="0000" + ftp_addr=nil + ftp_port=nil + ftp_username=nil + ftp_passwd=nil + db_dsn="SQLite3:#{BuildServer::CONFIG_ROOT}/#{id}/server.db" + db_user=nil + db_passwd=nil # read configuration server_dir = "#{BuildServer::CONFIG_ROOT}/#{id}" File.open( "#{server_dir}/server.cfg", "r" ) do |f| f.each_line do |l| + if l.strip.start_with?("#") then next end + idx = l.index("=") + 1 + length = l.length - idx + if l.start_with?("PATH=") - path = l.split("=")[1].strip - elsif l.start_with?("PSERVER_URL=") - pkgsvr_url = l.split("=")[1].strip - elsif l.start_with?("PSERVER_ADDR=") - pkgsvr_addr = l.split("=")[1].strip - elsif l.start_with?("PSERVER_ID=") - pkgsvr_id = l.split("=")[1].strip - elsif l.start_with?("PSERVER_CACHE_PATH=") - pkgsvr_cache_path = l.split("=")[1].strip - elsif l.start_with?("GIT_SERVER_URL=") - git_server_url = l.split("=")[1].strip + path = l[idx,length].strip elsif l.start_with?("GIT_BIN_PATH=") - git_bin_path = l.split("=")[1].strip - elsif l.start_with?("ALLOWED_GIT_BRANCH=") - allowed_git_branch = l.split("=")[1].strip - elsif l.start_with?("MAX_WORKING_JOBS=") - max_working_jobs = l.split("=")[1].strip.to_i - elsif l.start_with?("JOB_LOG_URL=") - job_log_url = l.split("=")[1].strip + git_bin_path = l[idx,length].strip + elsif l.start_with?("TEST_TIME=") + test_time = l[idx,length].strip.to_i + elsif l.start_with?("PASSWORD=") + password = l[idx,length].strip.to_i + elsif l.start_with?("FTP_URL=") + ftp_result = Utils.parse_ftpserver_url(l[idx,length].strip) + ftp_addr = ftp_result[0] + ftp_port = ftp_result[1] + ftp_username = ftp_result[2] + ftp_passwd = ftp_result[3] + elsif l.start_with?("DB_DSN=") + case l[idx,length].strip + when /^SQLite3:(.*)/i then + if $1.strip.empty? then db_dsn = "SQLite3:#{BuildServer::CONFIG_ROOT}/#{id}/server.db" + else db_dsn = "SQLite3:#{$1.strip}" + end + when /^Mysql:(.*)/i then + db_dsn = "Mysql:#{$1}" + else + db_dsn = "SQLite3:#{BuildServer::CONFIG_ROOT}/#{id}/server.db" + end + elsif l.start_with?("DB_USER=") + db_user = l[idx,length].strip if not l[idx,length].strip.empty? + elsif l.start_with?("DB_PASSWORD=") + db_passwd = l[idx,length].strip if not l[idx,length].strip.empty? else - next - end + next + end end end # create server object - obj = BuildServer.new( id, path, pkgsvr_url, pkgsvr_addr, pkgsvr_id ) + obj = BuildServer.new( id, path, ftp_addr, ftp_port, ftp_username, ftp_passwd ) # check running port if File.exist? "#{server_dir}/run" then @@ -337,40 +921,23 @@ class BuildServerController f.close end - # check friend server - if File.exist? "#{server_dir}/friends" then - File.open( "#{server_dir}/friends", "r" ) do |f| - f.each_line do |l| - if l.split(",").count < 2 then next end - ip = l.split(",")[0].strip - port = l.split(",")[1].strip - obj.add_friend_server( ip, port.to_i ) - end - end - end - - # set git server url - obj.git_server_url = git_server_url - # set git binary path obj.git_bin_path = git_bin_path - - # set git binary path - obj.max_working_jobs = max_working_jobs - # set job log url - obj.job_log_url = job_log_url + # set test time + obj.test_time = test_time - # set allowed git branch name - obj.allowed_git_branch = allowed_git_branch + # set password + obj.password = password - # set package server path - pkgsvr_cache_path = (pkgsvr_cache_path.empty? ? "#{path}/pkgsvr_cache":pkgsvr_cache_path) - obj.pkgsvr_cache_path= pkgsvr_cache_path + # DB settring + obj.db_dsn = db_dsn if not db_dsn.nil? + obj.db_user = db_user + obj.db_passwd = db_passwd # save config - write_server_config( obj ) - + #write_server_config( obj ) + # create object & return it return obj end diff --git a/src/build_server/BuildServerException.rb b/src/build_server/BuildServerException.rb new file mode 100644 index 0000000..8faed9a --- /dev/null +++ b/src/build_server/BuildServerException.rb @@ -0,0 +1,31 @@ +class BuildServerException < Exception + @@err_msgs = { + "ERR001" => "Invalid request format is used!", + "ERR002" => "Distribution not found!", + "ERR003" => "Unsupported OS name used!", + "ERR004" => "User account not found!", + "ERR005" => "Project access denied!", + "ERR006" => "Job creation failed!", + "ERR007" => "No supported OS defined in build server!", + "ERR008" => "Distribution locked!", + "ERR009" => "Project not found!", + "ERR010" => "Build operation not allowed on this project type!", + "ERR011" => "Project password required!", + "ERR012" => "Project password not matched!", + "ERR013" => "Project from file-name/distribution not found!", + "ERR014" => "Job cancel failed!", + "ERR015" => "Server password not matched!" + } + + def initialize(code) + @err_code = code + end + + def err_message() + if not message().nil? and not message().empty? then + return "Error: #{@@err_msgs[@err_code]}: #{message()}" + else + return "Error: #{@@err_msgs[@err_code]}" + end + end +end diff --git a/src/build_server/BuildServerOptionParser.rb b/src/build_server/BuildServerOptionParser.rb index 55fc370..455a48b 100644 --- a/src/build_server/BuildServerOptionParser.rb +++ b/src/build_server/BuildServerOptionParser.rb @@ -1,5 +1,5 @@ =begin - + BuildServerOptionParser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -26,92 +26,366 @@ Contributors: - S-Core Co., Ltd =end +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" require 'optparse' +require 'utils' + +class BuildServerUsage + CREATE="build-svr create -n [-t ]" + REMOVE="build-svr remove -n " + MIGRATE="build-svr migrate -n [--dsn [--dbuser --dbpassword ] ]" + START="build-svr start -n -p " + STOP="build-svr stop -n " + UPGRADE="build-svr upgrade -n [-D ]" + ADD_SVR="build-svr add-svr -n -d " + REMOVE_SVR= "build-svr remove-svr -n -d " + ADD_OS= "build-svr add-os -n -o " + REMOVE_OS="build-svr remove-os -n -o " + ADD_DIST= "build-svr add-dist -n -D -u -d " + REMOVE_DIST="build-svr remove-dist -n -D " + LOCK_DIST="build-svr lock-dist -n -D " + UNLOCK_DIST="build-svr unlock-dist -n -D " + ADD_SYNC="build-svr add-sync -n -u [--dist ]" + REMOVE_SYNC="build-svr remove-sync -n -u [--dist ]" + ADD_PRJ="build-svr add-prj -n -N (-g -b |-P ) [-w ] [-o ] [--dist ]" + REMOVE_PRJ="build-svr remove-prj -n -N [--dist ]" + FULLBUILD="build-svr fullbuild -n [--dist ]" + REGISTER="build-svr register -n -P [--dist ]" + QUERY="build-svr query -n " + SET_ATTR="build-svr set-attr -n -A -V " + GET_ATTR="build-svr get-attr -n -A " +end + +def option_error_check( options ) + case options[:cmd] + + when "create" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::CREATE + end + + when "remove" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE + end + + when "migrate" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::MIGRATE + end + + when "start" + if options[:name].nil? or options[:name].empty? or + options[:port].nil? then + raise ArgumentError, "Usage: " + BuildServerUsage::START + end + + when "stop" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::STOP + end + + when "upgrade" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::UPGRADE + end + + when "add-svr" + if options[:name].nil? or options[:name].empty? or + (options[:domain].nil? or options[:domain].empty?) then + raise ArgumentError, "Usage: " + BuildServerUsage::ADD_SVR + end + + when "remove-svr" + if options[:name].nil? or options[:name].empty? or + (options[:domain].nil? or options[:domain].empty?) then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE_SVR + end + + when "add-os" + if options[:name].nil? or options[:name].empty? or + options[:os].nil? or options[:os].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::ADD_OS + end + + when "remove-os" + if options[:name].nil? or options[:name].empty? or + options[:os].nil? or options[:os].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE_OS + end + + when "add-dist" + if options[:name].nil? or options[:name].empty? or + options[:dist].nil? or options[:dist].empty? or + options[:url].nil? or options[:url].empty? or + options[:domain].nil? or options[:domain].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::ADD_DIST + end + + when "remove-dist" + if options[:name].nil? or options[:name].empty? or + options[:dist].nil? or options[:dist].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE_DIST + end + + when "lock-dist" + if options[:name].nil? or options[:name].empty? or + options[:dist].nil? or options[:dist].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::LOCK_DIST + end + + when "unlock-dist" + if options[:name].nil? or options[:name].empty? or + options[:dist].nil? or options[:dist].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::UNLOCK_DIST + end + + when "add-sync" + if options[:name].nil? or options[:name].empty? or + (options[:url].nil? or options[:url].empty?) then + raise ArgumentError, "Usage: " + BuildServerUsage::ADD_SYNC + end + + when "remove-sync" + if options[:name].nil? or options[:name].empty? or + (options[:url].nil? or options[:url].empty?) then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE_SYNC + end + + when "add-prj" + if options[:name].nil? or options[:name].empty? or + options[:pid].nil? or options[:pid].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::ADD_PRJ + end + + when "remove-prj" + if options[:name].nil? or options[:name].empty? or + options[:pid].nil? or options[:pid].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::REMOVE_PRJ + end + + when "fullbuild" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::FULLBUILD + end + + when "register" + if options[:name].nil? or options[:name].empty? or + options[:package].nil? or options[:package].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::REGISTER + end + + when "query" + if options[:name].nil? or options[:name].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::QUERY + end + + when "set-attr" + if options[:name].nil? or options[:name].empty? or + options[:attr].nil? or options[:attr].empty? or + options[:value].nil? or options[:value].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::SET_ATTR + end + + when "get-attr" + if options[:name].nil? or options[:name].empty? or + options[:attr].nil? or options[:attr].empty? then + raise ArgumentError, "Usage: " + BuildServerUsage::SET_ATTR + end -def option_parse - options = {} - banner = "Usage: build-svr {create|remove|start|build|help} ..." + "\n" \ - + "\t" + "build-svr create -n -u -d -i " + "\n" \ - + "\t" + "build-svr remove -n " + "\n" \ - + "\t" + "build-svr start -n [-p [-l ] [-g -c ] [-o ] [-r]" + "\n" \ - + "\t" + "build-svr add -n [-d -p ]" + "\n" - - optparse = OptionParser.new do|opts| - # Set a banner, displayed at the top - # of the help screen. - - opts.banner = banner - - opts.on( '-n', '--name ', 'build server name' ) do|name| - options[:name] = name - end - - opts.on( '-u', '--url ', 'package server URL: http://xxx/yyy/zzz' ) do|url| - options[:url] = url - end - - opts.on( '-d', '--domain ', 'package svr or friend svr ip or ssh alias' ) do|domain| - options[:domain] = domain - end - - opts.on( '-i', '--id ', 'package server id' ) do|pid| - options[:pid] = pid - end - - options[:port] = 2222 - opts.on( '-p', '--port ', 'port' ) do|port| - options[:port] = port.strip.to_i - end - - opts.on( '-l', '--local ', 'local source path' ) do|path| - options[:local] = path - end - - opts.on( '-g', '--git ', 'git repository gerrithost:/xxx/yyy/zzz' ) do|git| - options[:git] = git - end - - opts.on( '-c', '--commit ', 'git commit id/tag' ) do|git| - if git.start_with? "/" then - git = git[1..-1] - end - options[:commit] = git - end - - opts.on( '-o', '--os ', 'target operating system linux/windows/darwin' ) do|os| - options[:os] = os - end - - options[:resolve] = false - opts.on( '-r', '--resolve', 'reverse build dependency fail resolve' ) do - options[:resolve] = true - end + else + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" + end +end + +def option_parse + options = {} + banner = "Build-server administer service command-line tool." + "\n" \ + + "\n" + "Usage: build-svr [OPTS] or build-svr (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "create Create the build-server." + "\n" \ + + "\t" + "remove Remove the build-server." + "\n" \ + + "\t" + "migrate build-server DB migrate." + "\n" \ + + "\t" + "start Start the build-server." + "\n" \ + + "\t" + "stop Stop the build-server." + "\n" \ + + "\t" + "upgrade Upgrade the build-server include friends." + "\n" \ + + "\t" + "add-svr Add remote build/package server for support multi-OS or distribute build job." + "\n" \ + + "\t" + "remove-svr Remove remote build/package server for support multi-OS or distribute build job." + "\n" \ + + "\t" + "add-os Add supported OS." + "\n" \ + + "\t" + "remove-os Remove supported OS." + "\n" \ + + "\t" + "add-dist Add distribution." + "\n" \ + + "\t" + "remove-dist Remove distribution." + "\n" \ + + "\t" + "lock-dist Lock distribution." + "\n" \ + + "\t" + "unlock-dist Unlock distribution." + "\n" \ + + "\t" + "add-sync Add package repository URL to synchronize with." + "\n" \ + + "\t" + "remove-sync Remove package repository URL." + "\n" \ + + "\t" + "add-prj Add project to build." + "\n" \ + + "\t" + "remove-prj Remove project." + "\n" \ + + "\t" + "register Register the package to the build-server." + "\n" \ + + "\t" + "fullbuild Build all your projects and upload them to package server." + "\n" \ + + "\t" + "query Show build server configuration." + "\n" \ + + "\t" + "set-attr Set build server atribute." + "\n" \ + + "\t" + "get-attr Get build server atribute." + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + BuildServerUsage::CREATE + "\n" \ + + "\t" + BuildServerUsage::REMOVE + "\n" \ + + "\t" + BuildServerUsage::MIGRATE + "\n" \ + + "\t" + BuildServerUsage::START + "\n" \ + + "\t" + BuildServerUsage::STOP + "\n" \ + + "\t" + BuildServerUsage::UPGRADE + "\n" \ + + "\t" + BuildServerUsage::ADD_SVR + "\n" \ + + "\t" + BuildServerUsage::REMOVE_SVR + "\n" \ + + "\t" + BuildServerUsage::ADD_OS + "\n" \ + + "\t" + BuildServerUsage::REMOVE_OS + "\n" \ + + "\t" + BuildServerUsage::ADD_DIST + "\n" \ + + "\t" + BuildServerUsage::REMOVE_DIST + "\n" \ + + "\t" + BuildServerUsage::LOCK_DIST + "\n" \ + + "\t" + BuildServerUsage::UNLOCK_DIST + "\n" \ + + "\t" + BuildServerUsage::ADD_SYNC + "\n" \ + + "\t" + BuildServerUsage::REMOVE_SYNC + "\n" \ + + "\t" + BuildServerUsage::ADD_PRJ + "\n" \ + + "\t" + BuildServerUsage::REMOVE_PRJ + "\n" \ + + "\t" + BuildServerUsage::FULLBUILD + "\n" \ + + "\t" + BuildServerUsage::REGISTER + "\n" \ + + "\t" + BuildServerUsage::QUERY + "\n" \ + + "\t" + BuildServerUsage::SET_ATTR + "\n" \ + + "\t" + BuildServerUsage::GET_ATTR + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + # Set a banner, displayed at the top + # of the help screen. + + opts.banner = banner + + opts.on( '-n', '--name ', 'build server name' ) do|name| + options[:name] = name + end + + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do|url| + options[:url] = url + end + + opts.on( '-d', '--address ', 'server address: 127.0.0.1:2224' ) do|domain| + options[:domain] = domain + end + + options[:port] = 2222 + opts.on( '-p', '--port ', 'server port number: 2224' ) do|port| + options[:port] = port.strip.to_i + end + + opts.on( '-P', '--pkg ', 'package file path or name' ) do|package| + options[:package] = package.strip + end + + options[:os] = nil + opts.on( '-o', '--os ', 'ex) ubuntu-32,windows-32' ) do|os| + if not Utils.multi_argument_test( os, "," ) then + raise ArgumentError, "OS variable parsing error : #{os}" + end + options[:os] = os + end + + opts.on( '-N', '--pname ', 'project name' ) do|pid| + options[:pid] = pid + end + + opts.on( '-g', '--git ', 'git repository' ) do|git| + options[:git] = git + end + + opts.on( '-b', '--branch ', 'git branch' ) do|branch| + options[:branch] = branch + end + + #opts.on( '-r', '--remote ', 'remote server id' ) do|remote| + # options[:remote] = remote + #end + options[:dist] = "" + opts.on( '-D', '--dist ', 'distribution name' ) do |dist| + options[:dist] = dist + end + + + options[:passwd] = "" + opts.on( '-w', '--passwd ', 'password for managing project' ) do|passwd| + options[:passwd] = passwd + end + + opts.on( '-t', '--ftp ', 'ftp server url: ftp://dibsftp:dibsftp@127.0.0.1:1024' ) do|domain| + options[:fdomain] = domain + end + + opts.on( '-A', '--attr ', 'attribute' ) do |attr| + options[:attr] = attr + end + + options[:db_dsn] = nil + opts.on( '--dsn ', 'Data Source Name ex) mysql:host=localhost;database=test' ) do |dsn| + options[:db_dsn] = dsn + end + + options[:db_user] = nil + opts.on( '--dbuser ', 'DB user id' ) do |user| + options[:db_user] = user + end + + options[:db_passwd] = nil + opts.on( '--dbpassword ', 'DB password' ) do |password| + options[:db_passwd] = password + end + + + opts.on( '-V', '--value ', 'value' ) do |value| + options[:value] = value + end opts.on( '-h', '--help', 'display this information' ) do - puts opts + opts.help.split("\n").each {|op| puts op if not op.include? "--CHILD"} + exit + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() exit - end - - end - - cmd = ARGV[0] + end + + opts.on( '-C', '--CHILD', 'child process' ) do + options[:child] = true + end - if cmd.eql? "create" or cmd.eql? "remove" or cmd.eql? "start" or - cmd.eql? "build" or cmd.eql? "add" or - cmd =~ /(help)|(-h)|(--help)/ then + end - if cmd.eql? "help" then - ARGV[0] = "-h" + cmd = ARGV[0] + if cmd.eql? "create" or cmd.eql? "remove" or + cmd.eql? "start" or cmd.eql? "upgrade" or + cmd.eql? "stop" or cmd.eql? "migrate" or + cmd.eql? "add-svr" or cmd.eql? "remove-svr" or + cmd.eql? "add-os" or cmd.eql? "remove-os" or + cmd.eql? "add-dist" or cmd.eql? "remove-dist" or + cmd.eql? "lock-dist" or cmd.eql? "unlock-dist" or + cmd.eql? "add-sync" or cmd.eql? "remove-sync" or + cmd.eql? "add-prj" or cmd.eql? "remove-prj" or + cmd.eql? "fullbuild" or cmd.eql? "register" or + cmd.eql? "query" or + cmd.eql? "set-attr" or cmd.eql? "get-attr" or + cmd =~ /(-v)|(--version)/ or + cmd =~ /(help)|(-h)|(--help)/ then + + if cmd.eql? "help" then + ARGV[0] = "-h" end - options[:cmd] = ARGV[0] - else - raise ArgumentError, banner - end + options[:cmd] = ARGV[0] + else + raise ArgumentError, "Usage: build-svr [OPTS] or build-svr -h" + end + + optparse.parse! + + option_error_check options - optparse.parse! - - return options -end + return options +end diff --git a/src/build_server/CommonJob.rb b/src/build_server/CommonJob.rb new file mode 100644 index 0000000..5a624df --- /dev/null +++ b/src/build_server/CommonJob.rb @@ -0,0 +1,277 @@ +=begin + + CommonJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "time" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils.rb" + +class CommonJob + + attr_accessor :id, :server, :log, :status, :priority + attr_accessor :os, :type, :pre_jobs, :user_id + USER_JOB_PRIORITY = 100 + AUTO_JOB_PRIORITY = 0 + + # initialize + public + def initialize(server, id=nil) + @server = server + if not server.jobmgr.nil? then + @id = server.jobmgr.get_new_job_id() + else + @id = 0 + end + + @parent = nil + @sub_jobs = [] + @priority = USER_JOB_PRIORITY # higher numbered job get priority + @os = "Unknown" + @type = "Unknown" + @pre_jobs = [] #pre-requisite jobs + @project = nil + @user_id = 1 + + @status = "JUST_CREATED" + @log = nil + + @start_time = Time.now + @end_time = nil + + @sub_pid = 0 + end + + + # set parent + public + def set_parent_job( parent ) + @parent = parent + end + + + # get parent + public + def get_parent_job() + return @parent + end + + + # check this job has a parent job + public + def is_sub_job? + return (not @parent.nil?) + end + + + # get all sub jobs + public + def get_sub_jobs + return @sub_jobs + end + + + # add sub job + public + def add_sub_job( job ) + @sub_jobs.push job + # this will make sub-job to share build-root of parent + job.set_parent_job( self ) + end + + + # create logger + public + def create_logger( second_out, verbose = false ) + @log = JobLog.new( self, second_out, verbose ) + return @log + end + + + public + def get_project() + return @project + end + + + public + def set_project( project ) + @project = project + end + + public + def get_user_email + return @server.get_email_using_user_id(@user_id) + end + + + # execute + public + def execute(sync=false) + + # create a thread for job + @thread = Thread.new do + begin + job_main() + + # parent job will call sub job's terminate method + if not is_sub_job? then terminate() end + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + end + + if sync then + @thread.join + end + + return true + end + + + #terminate + public + def terminate() + #do noting + end + + + #cancel + public + def cancel() + # kill sub process if exist? + kill_sub_process() + end + + + # show progress + public + def progress + # do nothing + return "" + end + + + # create process to execute command + public + def execute_command(cmd) + # execute + pid, status = Utils.execute_shell_with_log(cmd, @log.path, false) + @sub_pid = pid + + # wait for finish + begin + pid, status = Process.waitpid2(pid) + rescue Errno::ECHILD + # pid is not exist + # do notting + end + @sub_pid = 0 + + # return + return pid, status + end + + + public + def self.create_table(db, post_fix) + db.do "CREATE TABLE jobs ( + id INTEGER PRIMARY KEY, + project_id INTEGER, + user_id INTEGER NOT NULL, + supported_os_id INTEGER, + distribution_id INTEGER, + parent_job_id INTEGER, + remote_build_server_id INTEGER, + source_id INTEGER, + jtype VARCHAR(32) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'JUST_CREATED', + start_time DATETIME, + end_time DATETIME, + CONSTRAINT fk_jobs_projects1 FOREIGN KEY ( project_id ) REFERENCES projects ( id ), + CONSTRAINT fk_jobs_users1 FOREIGN KEY ( user_id ) REFERENCES users ( id ), + CONSTRAINT fk_jobs_supported_os1 FOREIGN KEY ( supported_os_id ) REFERENCES supported_os ( id ), + CONSTRAINT fk_jobs_distributions1 FOREIGN KEY ( distribution_id ) REFERENCES distributions ( id ), + CONSTRAINT fk_jobs_jobs1 FOREIGN KEY ( parent_job_id ) REFERENCES jobs ( id ), + CONSTRAINT fk_jobs_sources1 FOREIGN KEY ( source_id ) REFERENCES sources ( id ), + CONSTRAINT fk_jobs_remote_build_servers1 FOREIGN KEY ( remote_build_server_id ) REFERENCES remote_build_servers ( id ) )#{post_fix}" + end + + + # save to db + public + def save(db, now) + + prj_id = @project.nil? ? "NULL" : @project.get_project_id() + row=db.select_one("SELECT * FROM jobs WHERE id=#{@id}") + if row.nil? then + start_time = @start_time.strftime("%F %T") + os_id = BuildServer.get_supported_os_id(db, @os) + dist_id = PackageDistribution.get_distribution_id(db, get_distribution_name()) + parent_id = @parent.nil? ? "NULL" : @parent.id + db.do "INSERT INTO jobs(id,project_id,user_id,supported_os_id, distribution_id, parent_job_id,jtype,status,start_time) + VALUES (#{@id},#{prj_id},#{@user_id},#{os_id},#{dist_id},#{parent_id},'#{@type}','#{@status}',#{now})" + else + remote_bs_id = (@type == "BUILD" and not get_remote_server().nil?) ? + get_remote_server().id : "NULL" + if @status == "FINISHED" and not @project.nil? and + (@type == "BUILD" or @type == "REGISTER") then + source_id = @project.get_source_id_from_ver(pkginfo.get_version(),db) + db.do "UPDATE jobs SET source_id=#{source_id} WHERE id=#{@id}" + end + db.do "UPDATE jobs SET status='#{@status}',remote_build_server_id=#{remote_bs_id} WHERE id=#{@id}" + if @status == "FINISHED" or @status == "ERROR" or @status == "CANCELED" then + @end_time = Time.now.strftime("%F %T") + db.do "UPDATE jobs SET end_time=#{now} WHERE id=#{@id}" + end + end + end + + + # + # PROTECTED METHODS + # + + # main module + protected + def job_main + # do nothing + end + + + protected + def kill_sub_process() + if @sub_pid != 0 then + if not @log.nil? then + @log.info("Killing sub process! id = #{@sub_pid}") + end + Utils.kill_process(@sub_pid) + end + end +end diff --git a/src/build_server/CommonProject.rb b/src/build_server/CommonProject.rb new file mode 100644 index 0000000..a2b46ac --- /dev/null +++ b/src/build_server/CommonProject.rb @@ -0,0 +1,355 @@ +=begin + + CommonProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" + +class CommonProject + attr_accessor :name, :type, :passwd, :os_list, :dist_name, :path + + # initialize + def initialize( name, type, server, os_list, dist_name="BASE" ) + @prj_id = -1 + @name = name + @type = type + @passwd = "" + @os_list = os_list + @server = server + @dist_name = dist_name + @source_info = {} + @package_info = {} + if @dist_name == "BASE" then + @path = "#{@server.path}/projects/#{@name}" + else + @path = "#{@server.path}/projects/#{@dist_name}/#{@name}" + end + end + + + def ==(y) + @name == y.name and @dist_name = y.dist_name + end + + + def init() + # create project directory if not exist + if not File.exist? @path then FileUtils.mkdir_p @path end + end + + + #return passwd + def is_passwd_set?() + return ( not @passwd.empty? ) + end + + + def passwd_match?(word) + if not is_passwd_set? then return true end + + if word.eql? @passwd then + return true + else + return false + end + end + + + def set_source_info(info) + @source_info = info + end + + + def save_source_info(ver, info) + @source_info[ver] = info + + # save to db + @server.get_db_connection() do |db| + save_source_info_internal(ver, info, db) + end + end + + + def get_source_info(ver) + return @source_info[ver] + end + + + def set_package_info(info) + @package_info = info + end + + + def save_package_info(ver, pkg_name, pkg_os) + if @package_info[ver].nil? then + @package_info[ver] = [] + end + @package_info[ver].push [pkg_name, pkg_os] + + # save to db + @server.get_db_connection() do |db| + save_package_info_internal(ver, pkg_name, pkg_os, db) + end + end + + + # get latest package version + def get_latest_version() + versions = @package_info.keys + if not versions.empty? then + versions.sort! {|x,y| Version.new(x).compare(Version.new(y)) } + return versions[-1] + else + return nil + end + end + + + # get all package version + def get_all_versions() + return @package_info.keys + end + + + def include_package?(pkg_name, ver=nil, os=nil) + # check version first + if not ver.nil? then + ver = get_latest_version() + end + + if ver.nil? or @package_info[ver].nil? then return false end + if not os.nil? and not @os_list.include? os then return false end + + # check name and version + @package_info[ver].each do |pkg| + if pkg_name == pkg[0] and os == pkg[1] then + return true + end + end + + return false + end + + + def set_project_id(id) + @prj_id = id + end + + + def get_project_id() + return @prj_id + end + + def self.create_table(db, inc, post_fix) + db.do "CREATE TABLE projects ( + id INTEGER PRIMARY KEY #{inc}, + distribution_id INTEGER NOT NULL, + user_id INTEGER, + name VARCHAR(32) NOT NULL, + ptype VARCHAR(32) NOT NULL, + password VARCHAR(32), + CONSTRAINT fk_projects_users1 FOREIGN KEY ( user_id ) REFERENCES users ( id ), + CONSTRAINT fk_projects_distributions1 FOREIGN KEY ( distribution_id ) REFERENCES distributions ( id ) )#{post_fix}" + + db.do "CREATE TABLE project_os ( + project_id INTEGER NOT NULL, + supported_os_id INTEGER NOT NULL, + PRIMARY KEY ( project_id,supported_os_id ), + CONSTRAINT fk_projects_has_supported_os_projects FOREIGN KEY ( project_id ) REFERENCES projects ( id ), + CONSTRAINT fk_projects_has_supported_os_supported_os1 FOREIGN KEY ( supported_os_id ) REFERENCES supported_os ( id ) )#{post_fix}" + + db.do "CREATE TABLE sources ( + id INTEGER PRIMARY KEY #{inc}, + project_id INTEGER NOT NULL, + pkg_ver VARCHAR(64) NOT NULL, + location VARCHAR(256) NOT NULL, + CONSTRAINT fk_project_sources_projects1 FOREIGN KEY ( project_id ) REFERENCES projects ( id ))#{post_fix}" + + db.do "CREATE TABLE packages ( + id INTEGER PRIMARY KEY #{inc}, + source_id INTEGER NOT NULL, + supported_os_id INTEGER NOT NULL, + pkg_name VARCHAR(64) NOT NULL, + CONSTRAINT fk_project_packages_project_sources1 FOREIGN KEY ( source_id ) REFERENCES sources ( id ), + CONSTRAINT fk_project_packages_supported_os1 FOREIGN KEY ( supported_os_id ) REFERENCES supported_os ( id ) )#{post_fix}" + + end + + + protected + def self.load_row(name, dist_name, db) + row = db.select_one("SELECT projects.* FROM projects,distributions WHERE projects.name='#{name}' and + projects.distribution_id=distributions.id and distributions.name='#{dist_name}'") + if row.nil? then return nil end + + # get supported_os + prj_id = row['id'] + os_list = [] + rows = db.select_all("SELECT supported_os.name FROM project_os,supported_os WHERE project_id=#{prj_id} and supported_os.id = project_os.supported_os_id") + rows.each do |r| + os_list.push r['name'] + end + + # get source info/ package info + source_info = {} + package_info = {} + rows=db.select_all("SELECT * FROM sources WHERE project_id=#{prj_id}") + rows.each do |r| + source_info[r['pkg_ver']] = r['location'] + + source_id = r['id'] + rows2=db.select_all("SELECT packages.pkg_name,supported_os.name as os_name + FROM packages,supported_os WHERE source_id=#{source_id} and packages.supported_os_id=supported_os.id") + rows2.each do |r2| + if package_info[r['pkg_ver']].nil? then + package_info[r['pkg_ver']] = [] + end + package_info[r['pkg_ver']].push [r2['pkg_name'], r2['os_name']] + end + end + + return row, os_list, source_info, package_info + end + + + protected + def save_common(db) + if @prj_id == -1 then + row = db.select_one("SELECT * FROM distributions WHERE name='#{@dist_name}'") + dist_id = row['id'] + db.do "INSERT INTO projects (distribution_id,name,ptype,password) + VALUES (#{dist_id},'#{@name}','#{@type}','#{@passwd}')" + case @server.db_dsn + when /^SQLite3:/ then @prj_id = db.select_one("select last_insert_rowid()")[0] + when /^Mysql:/ then @prj_id = db.func(:insert_id) + else @prj_id = db.select_one("select last_insert_rowid()")[0] + end + @os_list.each do |os| + row = db.select_one("SELECT * FROM supported_os WHERE name='#{os}'") + os_id = row['id'] + db.do "INSERT INTO project_os VALUES(#{@prj_id},#{os_id})" + end + + return true + else + row = db.select_one("SELECT * FROM distributions WHERE name='#{@dist_name}'") + dist_id = row['id'] + db.do "UPDATE projects SET ptype='#{@type}',password='#{@passwd}' WHERE name='#{@name}' and distribution_id=#{dist_id})" + db.do "DELETE FROM project_os WHERE project_id=#{@prj_id}" + @os_list.each do |os| + row = db.select_one("SELECT * FROM supported_os WHERE name='#{os}'") + os_id = row['id'] + db.do "INSERT INTO project_os VALUES(#{@prj_id},#{os_id})" + end + + @source_info.each do |src_ver,info| + save_source_info_internal(src_ver, info, db) + end + + @package_info.each do |src_ver,pkg_name_os_pair| + pkg_name_os_pair.each do |pkg_name, pkg_os| + save_package_info_internal(src_ver, pkg_name, pkg_os, db) + end + end + + return false + end + end + + + public + def get_source_id_from_ver( src_ver, db ) + row=db.select_one("SELECT * FROM sources WHERE project_id=#{@prj_id} and pkg_ver='#{src_ver}'") + if row.nil? then + return "NULL" + else + return row['id'] + end + end + + + protected + def save_source_info_internal(src_ver, info, db) + row1=db.select_one("SELECT * FROM sources WHERE project_id=#{@prj_id} and pkg_ver='#{src_ver}'") + if row1.nil? then + db.do "INSERT INTO sources(project_id, pkg_ver,location) + VALUES(#{@prj_id},'#{src_ver}','#{info}')" + end + end + + + protected + def save_package_info_internal(src_ver, pkg_name, pkg_os, db) + row=db.select_one("SELECT * FROM sources WHERE project_id=#{@prj_id} and pkg_ver='#{src_ver}'") + source_id = row['id'] + row = db.select_one("SELECT * FROM supported_os WHERE name='#{pkg_os}'") + os_id = row['id'] + row1=db.select_one("SELECT * FROM packages WHERE source_id=#{source_id} and pkg_name='#{pkg_name}' and supported_os_id=#{os_id}") + if row1.nil? then + db.do "INSERT INTO packages(source_id,supported_os_id,pkg_name) VALUES(#{source_id},#{os_id},'#{pkg_name}')" + end + end + + + # return its prject id and if not exist?, return -1 + protected + def unload_common(db) + row = db.select_one("SELECT * FROM projects WHERE id=#{@prj_id}") + if row.nil? then return -1 end + db.do "DELETE FROM project_os WHERE project_id=#{@prj_id}" + rows=db.select_all("SELECT * FROM sources WHERE project_id=#{@prj_id}") + rows.each do |r| + source_id = r['id'] + db.do "DELETE FROM packages WHERE source_id=#{source_id}" + end + db.do "DELETE FROM sources WHERE project_id=#{@prj_id}" + db.do("DELETE FROM projects WHERE id=#{@prj_id}") + end + + + public + def self.get_project_row(name, dist_name, db) + return db.select_one("SELECT * FROM projects WHERE name='#{name}' AND distribution_id=(SELECT id FROM distributions WHERE name='#{dist_name}')") + end + + public + def self.get_project_from_pkg_name_row(pkg_name, dist_name, db) + return db.select_one("SELECT projects.id,projects.distribution_id,projects.name,projects.ptype,projects.password + FROM distributions,projects,project_bins + WHERE distributions.name='#{dist_name}' and + distributions.id = projects.distribution_id and + projects.id = project_bins.project_id and + project_bins.pkg_name = '#{pkg_name}'") + end + + public + def self.get_all_project_rows(db) + return db.select_all("SELECT projects.name,distributions.name as dist_name,projects.ptype + FROM projects,distributions WHERE projects.distribution_id=distributions.id") + end +end diff --git a/src/build_server/DistributionManager.rb b/src/build_server/DistributionManager.rb new file mode 100644 index 0000000..2550496 --- /dev/null +++ b/src/build_server/DistributionManager.rb @@ -0,0 +1,227 @@ +=begin + + DistributionManager.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'thread' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "SocketJobRequestListener.rb" +require "RemoteBuildJob.rb" +require "RegisterPackageJob.rb" +require "packageServer.rb" + +class PackageDistribution + attr_accessor :name, :pkgsvr_url, :pkgsvr_ip, :pkgsvr_port, :status, :description, :id + + def initialize( name, pkgsvr_url, pkgsvr_ip, pkgsvr_port, status ) + @id = -1 + @name = name + @pkgsvr_url = pkgsvr_url + @pkgsvr_ip = pkgsvr_ip + @pkgsvr_port = pkgsvr_port + @status = status + @description = "" + end + + + def self.create_table(db, inc, post_fix) + db.do "CREATE TABLE distributions ( + id INTEGER PRIMARY KEY #{inc}, + name VARCHAR(32) NOT NULL UNIQUE, + pkgsvr_url VARCHAR(256), + pkgsvr_addr VARCHAR(64), + status VARCHAR(32) NOT NULL DEFAULT 'OPEN', + description VARCHAR(256) ) #{post_fix}" + end + + + def self.load(name, db) + row = db.select_one("SELECT * FROM distributions WHERE name='#{name}'") + return ( row.nil? ) ? nil : load_row(row) + end + + + def unload(db) + #TODO remove sync_pkg_server + #TODO remove projects + #TODO remove jobs + db.do("DELETE FROM distributions WHERE name='#{@name}'") + end + + + def self.load_all(db) + rows = db.select_all("SELECT * FROM distributions") + return rows.map{|x| load_row(x)} + end + + + def self.load_first(db) + row = db.select_one("SELECT * FROM distributions ORDER BY id") + return ( row.nil? ) ? nil : load_row(row) + end + + + def self.load_row(row) + pkgsvr_ip = row['pkgsvr_addr'].split(":")[0] + pkgsvr_port = row['pkgsvr_addr'].split(":")[1].to_i + new_obj = new(row['name'], row['pkgsvr_url'], pkgsvr_ip, pkgsvr_port, row['status']) + new_obj.description = row['description'] + new_obj.id = row['id'] + + return new_obj + end + + + def save(db) + dist_addr = @pkgsvr_ip + ":" + @pkgsvr_port.to_s + row = db.select_one("SELECT * FROM distributions WHERE name='#{@name}'") + if row.nil? then + db.do "INSERT INTO distributions(name, pkgsvr_url, pkgsvr_addr, status, description) VALUES ('#{@name}','#{@pkgsvr_url}','#{dist_addr}','#{@status}','#{@description}')" + else + db.do "UPDATE distributions SET pkgsvr_url='#{@pkgsvr_url}', pkgsvr_addr='#{dist_addr}', status='#{@status}', description='#{@description}' WHERE name='#{@name}'" + end + end + + + def self.get_distribution_id(db, dist_name) + row = db.select_one("SELECT * FROM distributions WHERE name='#{dist_name}'") + return ( row.nil? ) ? "NULL" : row['id'] + end +end + + +class DistributionManager + + # initialize + def initialize( server ) + @server = server + end + + + # get default distribution + def get_default_distribution_name() + dist = get_first_distribution() + return ( dist.nil? ) ? nil : dist.name + end + + + def get_default_pkgsvr_url() + dist = get_first_distribution() + return ( dist.nil? ) ? "" : dist.pkgsvr_url + end + + + # get distribution + def get_distribution(name) + # conntect DB + @server.get_db_connection() do |db| + return get_distribution_internal(name, db) + end + return nil + end + + + def get_distribution_internal(name, db) + return PackageDistribution.load(name, db) + end + + + # add + def add_distribution(name, pkgsvr_url, pkgsvr_ip, pkgsvr_port) + @server.get_db_connection() do |db| + if not get_distribution_internal(name, db).nil? then + @server.log.info "The distribution \"#{name}\" already exists on server" + @server.log.error "Adding distribution failed!" + return false + end + new_dist = PackageDistribution.new(name, pkgsvr_url, pkgsvr_ip, pkgsvr_port, "OPEN") + new_dist.save(db) + end + @server.log.info "Added a new distribution \"#{name}\"" + return true + end + + + # remove + def remove_distribution(name) + + result = @server.get_db_connection() do |db| + dist = get_distribution_internal(name, db) + if dist.nil? then + @server.log.error "The distribution \"#{name}\" does not exists on server" + @server.log.error "Removing distribution failed!" + return false + end + dist.unload(db) + end + + @server.log.info "Removed the distribution \"#{name}\"" + return result + end + + + def get_first_distribution() + @server.get_db_connection() do |db| + return PackageDistribution.load_first(db) + end + + return nil + end + + + def get_all_distributions() + @server.get_db_connection() do |db| + return PackageDistribution.load_all(db) + end + + return [] + end + + + def set_distribution_lock(name, value=true) + result = @server.get_db_connection() do |db| + # check already exist + dist = get_distribution_internal(name, db) + if dist.nil? then return false end + + dist.status = (value)? "CLOSE" : "OPEN" + dist.save(db) + end + if result then + if value then + @server.log.info "The distribution \"#{name}\" is locked!" + else + @server.log.info "The distribution \"#{name}\" is unlocked!" + end + end + + return result + end + + #END +end diff --git a/src/build_server/GitBuildJob.rb b/src/build_server/GitBuildJob.rb index d328925..301d810 100644 --- a/src/build_server/GitBuildJob.rb +++ b/src/build_server/GitBuildJob.rb @@ -1,5 +1,5 @@ =begin - + GitBuildJob.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -27,194 +27,286 @@ Contributors: =end require "fileutils" +require "thread" $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "BuildJob.rb" require "utils.rb" + +$git_mutex = Mutex.new + class GitBuildJob < BuildJob - attr_accessor :id, :status, :pkginfo, :pkgsvr_client, :thread, :log, :rev_fail_list, :rev_success_list, :source_path + attr_accessor :git_commit, :git_branch, :git_repos # initialize - def initialize ( repos, commit, os, pkgsvr_url, options, server, parent, outstream, resolve) - super() - @rev_fail_list = [] - @rev_success_list = [] - @id = server.get_new_job_id() - @server = server - @parent = parent - @git_repos = repos - @git_commit = commit - @os = os - @host_os = Utils::HOST_OS - if not pkgsvr_url.nil? and not pkgsvr_url.empty? then - @pkgserver_url = pkgsvr_url - else - local_pkgsvr = @server.local_pkgsvr - @pkgserver_url = local_pkgsvr.location + "/" + local_pkgsvr.get_default_dist_name - end - @options = options - @resolve = resolve - @outstream = outstream - - @status = "JUST_CREATED" - @sub_jobs = [] - @job_root = "#{@server.path}/jobs/#{@id}" - @source_path = @job_root+"/temp" - @pkginfo = nil - @job_working_dir=@job_root+"/works" - - @thread = nil - - # mkdir - FileUtils.rm_rf "#{@server.path}/jobs/#{@id}" - FileUtils.mkdir_p "#{@server.path}/jobs/#{@id}" - - # create logger - @log = JobLog.new(self,"#{@server.path}/jobs/#{@id}/log", outstream ) + def initialize( project, os, server ) + super(project, os, server) + @git_repos = project.repository + @git_branch = project.branch + @git_commit = nil end def terminate() - # report error if @status == "ERROR" then @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + elsif @status == "CANCELED" then + if not @log.nil? then + @log.error( "Job is CANCELED" , Log::LV_USER) + end + @server.cleaner.clean_afterwards(@id) else - # if succeeded, clean up - FileUtils.rm_rf "#{@source_path}" - FileUtils.rm_rf "#{@job_working_dir}" + @log.info( "Job is FINISHED successfully!" , Log::LV_USER) + + # if succeeded, register source info and copy pkginfo.manifest + @log.info( "Updating the source info for project \"#{@project.name}\"" , Log::LV_USER) + @project.save_source_info( @pkginfo.get_version(), @git_commit) + @project.save_package_info_from_manifest( @pkginfo.get_version(), + "#{@source_path}/package/pkginfo.manifest", @os) + @server.jobmgr.save_job_status(self) + # clean up + @server.cleaner.clean(@id) + end + + # clean up builder directory if exist? + if Builder.exist? "JB#{@id}" then + Builder.remove("JB#{@id}") end - # send mail - if ( not @pkginfo.nil? ) and not ( @pkginfo.packages.nil? ) then + # send mail + if ( @server.send_mail.eql? "YES" ) and ( not @pkginfo.nil? ) and ( not @pkginfo.packages.nil? ) then mail_list = [] contents = [] - done_pkg_list = [] contents.push " " contents.push " Git information : #{@git_commit} " contents.push " " contents.push "%-30s| %10s | %10s" % ["package name", "version", "os"] contents.push "---------------------------------------------------------------" - for pkg in @pkginfo.packages - if not pkg.os.eql? @os then next end - mail_list = mail_list | Mail.parse_email( pkg.maintainer ) + @pkginfo.packages.each do |pkg| + if not pkg.os.eql? @os then next end + mail_list = mail_list | Mail.parse_email( pkg.maintainer ) contents.push("%-30s| %10s | %10s" % [ pkg.package_name, pkg.version, pkg.os] ) end - - if @status == "ERROR" then - subject = "[DIBS] Build fail" + + if @status == "ERROR" then + subject = "[DIBS] Build fail" contents.push " " contents.push "check log file" contents.push "* Log : #{@server.job_log_url}/#{@id}/log" + elsif @status == "CANCELED" then + subject = "[DIBS] Build canceled" else - subject = "[DIBS] Build success" + subject = "[DIBS] Build success" end - #Mail.send_mail(mail_list, subject, contents.join("\n")) - end + Mail.send_mail(mail_list, subject, contents.join("\n")) + end # close logger @log.close + end - # send END signal , if connectionn is valid - if @status != "ERROR" and not @outstream.nil? then - BuildCommServer.send_end(@outstream) - end - # close outstream - if not @outstream.nil? then - BuildCommServer.disconnect( @outstream ) + # verify + def init + # mkdir job root + if not File.exist? @job_root then FileUtils.mkdir_p @job_root end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) end - end + @log.info( "Initializing job...", Log::LV_USER) - # verify - def pre_verify - @log.info( "Verifying job input...", Log::LV_USER) + # if internal job, copy external_pkgs + if @is_internal_job then + @log.info( "Copying external dependent pkgs...", Log::LV_USER) + ext_pkgs_dir = "#{@job_root}/external_pkgs" + + incoming_dir = "#{@server.transport_path}/#{@dock_num}" + if File.exist? incoming_dir then + FileUtils.mv "#{incoming_dir}", "#{ext_pkgs_dir}" + end + + FileUtils.mkdir_p incoming_dir + end - # git clone - if not git_cmd("clone #{@server.git_server_url}#{@git_repos} temp", @job_root) then - @log.error( "Failed on \"git clone #{@server.git_server_url}/#{@git_repos}\"", Log::LV_USER) + # download source code + @git_commit = get_source_code() + if @git_commit.nil? then @status = "ERROR" return false end - # git reset - if not git_cmd("reset --hard #{@git_commit}", @source_path) then - @log.error( "Failed on \"git reset --hard #{@git_commit}\"", Log::LV_USER) + # check pkginfo.manifest + if not File.exist? "#{@source_path}/package/pkginfo.manifest" + @log.error( "package/pkginfo.manifest does not exist", Log::LV_USER) @status = "ERROR" return false end - # check branch name if ALLOWED_GIT_BRANCH is not empty - if not @server.allowed_git_branch.empty? then - is_correct_branch = false - - # origin/{branch_name} - if @git_commit == "origin/#{@server.allowed_git_branch}" then - is_correct_branch = true - else - # get commit id - commit_id = "" - result_line = git_cmd_return("log -1",@source_path) - if result_line != nil then - result_line.each do |l| - if l.start_with?("commit ") then - commit_id = l.split(" ")[1].strip - end - end - end - - # check branch - if not commit_id.empty? and commit_id.length == 40 then - result_line = git_cmd_return("branch --contains=#{commit_id} -r", @source_path) - result_line.each do |l| - if l.include? "origin/#{@server.allowed_git_branch}" then - is_correct_branch = true - end - end - end + # set up pkg info + begin + @pkginfo = PackageManifest.new("#{@source_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + + # set up pkgsvr_client + @pkgsvr_client = Client.new(@pkgsvr_url, @job_working_dir, @log) + + # checking version if not reverse-build job or not internal-job + if not @is_rev_build_check_job and not @is_internal_job then + + #check change log + change_log = {} + begin + change_log = Parser.read_changelog "#{@source_path}/package/changelog" if File.exist? "#{@source_path}/package/changelog" + rescue => e + @log.error( e.message, Log::LV_USER) + return false end - if not is_correct_branch then - @log.error( "Wrong branch is used! Check your commit-id again", Log::LV_USER) + if not change_log.empty? and @pkginfo.packages[0].change_log.empty? then + @pkginfo.packages.each {|pkg| pkg.change_log = change_log} + end + + if @server.changelog_check and not @pkginfo.packages[0].does_change_exist? then + @log.error( "change log not found", Log::LV_USER ) + return false + end + + # check availabiltiy + if not @server.check_job_availability( self ) then + @log.error( "No servers that are able to build your packages.", Log::LV_USER) + @log.error( "Host-OS (#{@os}) is not supported in build server.", Log::LV_USER) + @status = "ERROR" + @server.log.info "Adding the job \"#{@id}\" is canceled" + return false + end + + if not check_package_version(@git_commit) then @status = "ERROR" return false end end - - # check pkginfo.manifest - if not File.exist? "#{@source_path}/package/pkginfo.manifest" - @log.error( "package/pkginfo.manifest doest not exist", Log::LV_USER) - @status = "ERROR" - return false + return true + end + + + # + # PROTECTED/PRIVATE METHODS + # + + protected + def get_source_code() + $git_mutex.synchronize do + get_source_code_internal() end + end - # set up pkg info - @pkginfo = PackageManifest.new("#{@source_path}/package/pkginfo.manifest") - # set up pkgsvr_client - @pkgsvr_client = Client.new(@pkgserver_url, @job_working_dir, @log) - @pkgsvr_client.update + protected + def get_source_code_internal() + # check git directory + git_path = "#{@project.path}/cache/git" + cache_path = "#{@project.path}/cache" + if not File.exist? cache_path then + FileUtils.mkdir_p cache_path + end - return true + # verify git & check branch name + if File.exist? git_path then + std_out_lines = git_cmd_return( "branch", git_path) + if std_out_lines.nil? then + @log.warn( "Git cache is corrupted! : #{@project.name}", Log::LV_USER) + FileUtils.rm_rf git_path + else + branch_list = std_out_lines.select{|x| x.start_with?("*")} + if branch_list.count == 0 then + @log.warn( "Git cache is corrupted! : #{@project.name}", Log::LV_USER) + FileUtils.rm_rf git_path + else + current_branch = branch_list[0].split(" ")[1].strip + if current_branch != @git_branch then + @log.warn( "Branch name is changed.", Log::LV_USER) + FileUtils.rm_rf git_path + end + end + end + end + + # git pull operation + if File.exist? git_path and not git_cmd("pull", git_path,@log) then + @log.warn( "Failed on \"git pull\"", Log::LV_USER) + FileUtils.rm_rf git_path + end + + # if no git, clone it + if not File.exist? git_path then + # if "git pull" failed, try to "git clone" + if not git_cmd("clone #{@git_repos} git", cache_path, @log) then + @log.error( "Failed on \"git clone #{@git_repos}\"", Log::LV_USER) + return nil + end + # git checkout + if not git_cmd("checkout #{@git_branch}", git_path, @log) then + @log.error( "Failed on \"git checkout #{@git_branch}\"", Log::LV_USER) + return nil + end + end + + if @git_commit.nil? then + # get git commit-id + commit_id = "" + result_line = git_cmd_return("log -1", git_path) + if result_line != nil then + result_line.each do |l| + if l.start_with?("commit ") then + commit_id = l.split(" ")[1].strip + end + end + end + + @git_commit = commit_id + else + # git reset + if not git_cmd("reset --hard #{@git_commit}", git_path, @log) then + @log.error( "Failed on \"git reset --hard #{@git_commit}\"", Log::LV_USER) + return nil + end + end + + # copy to source path + Utils.execute_shell_return( "cp -r #{git_path} #{@source_path}" ) + + return @git_commit end - def git_cmd(cmd, working_dir) - build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" - ret = Utils.execute_shell_with_log(build_command,@log) - - return ret + protected + def git_cmd(cmd, working_dir, log) + build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" + + pid, status = execute_command( build_command ) + if not status.nil? and status.exitstatus != 0 then + return false + else + return true + end end - def git_cmd_return(cmd, working_dir) - build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" + protected + def git_cmd_return(cmd, working_dir) + build_command = "cd \"#{working_dir}\";#{@server.git_bin_path} #{cmd}" ret = Utils.execute_shell_return(build_command) - + return ret end - end diff --git a/src/build_server/GitBuildProject.rb b/src/build_server/GitBuildProject.rb new file mode 100644 index 0000000..589651b --- /dev/null +++ b/src/build_server/GitBuildProject.rb @@ -0,0 +1,154 @@ +=begin + + GitProject.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'dbi' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "CommonProject.rb" +require "GitBuildJob.rb" +require "Version.rb" +require "PackageManifest.rb" + +# mutax for git operation + + +class GitBuildProject < CommonProject + attr_accessor :repository, :branch + + # initialize + def initialize( name, server, os_list, dist_name, repos = nil, branch = nil ) + super(name, "GIT", server, os_list, dist_name) + @repository = repos + @branch = branch + end + + + # get commid-id of specified package version + def get_commit_id( version ) + return get_source_info( version ) + end + + + # create new job + # if this project cannot support os, return nil + def create_new_job( os ) + if @os_list.include? os then + return GitBuildJob.new( self, os, @server ) + else + return nil + end + end + + + # create new job + def create_new_job_from_version( os, version=nil ) + new_job=create_new_job( os ) + + # set commit id + if version.nil? then + version = get_latest_version() + end + + commit = get_commit_id( version ) + if not commit.nil? then + new_job.git_commit = commit + end + + return new_job + end + + + # save package info + def save_package_info_from_manifest(version, file_path, os) + begin + pkginfo =PackageManifest.new(file_path) + rescue => e + @server.log.error e.message + return + end + + pkginfo.get_target_packages(os).each do |pkg| + save_package_info(pkg.version, pkg.package_name, os) + end + end + + + def self.create_table(db, post_fix) + db.do "CREATE TABLE project_gits ( + project_id INTEGER NOT NULL, + git_repos VARCHAR(128) NOT NULL, + git_branch VARCHAR(32) NOT NULL, + PRIMARY KEY ( project_id ), + CONSTRAINT fk_project_gits_projects1 FOREIGN KEY ( project_id ) REFERENCES projects ( id ) )#{post_fix}" + end + + + def self.load(name, dist_name, server, db) + row, prj_os_list, source_info, package_info = load_row(name, dist_name, db) + if row.nil? then return nil end + + prj_id = row['id'] + prj_name = row['name'] + prj_passwd = row['password'] + + new_project = GitBuildProject.new(prj_name, server, prj_os_list, dist_name) + if not prj_passwd.empty? then new_project.passwd = prj_passwd end + new_project.set_project_id( prj_id ) + new_project.set_source_info( source_info ) + new_project.set_package_info( package_info ) + + row=db.select_one("SELECT * FROM project_gits WHERE project_id=#{prj_id}") + if row.nil? then return nil end + new_project.repository=row['git_repos'] + new_project.branch=row['git_branch'] + + return new_project + end + + + def save(db) + is_new = save_common(db) + init() + + if is_new then + db.do "INSERT INTO project_gits VALUES (#{@prj_id},'#{@repository}','#{@branch}')" + db.do "INSERT INTO group_project_accesses + VALUES ( (SELECT groups.id FROM groups WHERE groups.name = 'admin'),'#{@prj_id}','TRUE')" + else + db.do "UPDATE project_gits SET git_repos='#{@repository}',git_branch='#{@branch}' WHERE project_id=#{@prj_id})" + end + end + + + def unload(db) + unload_common(db) + if @prj_id != -1 then + db.do "DELETE FROM project_gits WHERE project_id=#{@prj_id}" + end + end +end diff --git a/src/build_server/JobClean.rb b/src/build_server/JobClean.rb new file mode 100644 index 0000000..54d8597 --- /dev/null +++ b/src/build_server/JobClean.rb @@ -0,0 +1,199 @@ +=begin + + JobClean.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "Action.rb" +require "ScheduledActionHandler.rb" + +$access_listfile = Mutex.new + +class JobCleanAction < Action + + def initialize( time, job_path, list_file, server ) + super(time,0) + + @job_path = job_path + @job_id = @job_path.split("/")[-1] + @list_file = list_file + @server = server + end + + + def init + $access_listfile.synchronize do + File.open(@list_file, "a") do |f| + f.puts "#{@job_id},#{time.year},#{time.month},#{time.day},#{time.hour},#{time.min},#{time.sec}" + end + end + end + + + def execute + # Start to clean job + @server.log.info "Executing clean action for the job #{@job_id}" + begin + execute_internal() + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # remove directories + if File.exist? "#{@job_path}/buildroot" then + FileUtils.rm_rf "#{@job_path}/buildroot" + end + if File.exist? "#{@job_path}/temp" then + FileUtils.rm_rf "#{@job_path}/temp" + end + if File.exist? "#{@job_path}/external_pkgs" then + FileUtils.rm_rf "#{@job_path}/external_pkgs" + end + + # remove line for the job + $access_listfile.synchronize do + lines = [] + # get all lines + if File.exist? @list_file then + File.open(@list_file,"r") do |f| + f.each_line do |l| + lines.push l + end + end + end + + # write the line except my job_id + File.open(@list_file,"w") do |f| + lines.each do |l| + if l.split(",")[0].eql? @job_id then next end + f.puts l + end + end + end + end +end + + +class JobCleaner + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + @list_file = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/clean" + end + + + # start thread + def start() + + list_file2 = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/clean_backup" + jobs_path = "#{@server.path}/jobs" + if not File.exist? jobs_path then + FileUtils.mkdir_p jobs_path + end + + # read clean list + clean_list = [] + if File.exist? @list_file then + FileUtils.mv(@list_file,list_file2) + File.open(list_file2, "r") do |f| + f.each_line do |l| + id = l.split(",")[0] + year = l.split(",")[1] + month = l.split(",")[2] + day = l.split(",")[3] + hour = l.split(",")[4] + min = l.split(",")[5] + sec = l.split(",")[6] + + # create job and register + job_path = "#{jobs_path}/#{id}" + time = Time.mktime(year.to_i, month.to_i, day.to_i, hour.to_i, min.to_i, sec.to_i) + @server.log.info "Registered clean-action for the job in list : #{id}" + @handler.register(JobCleanAction.new(time,job_path,@list_file, @server)) + + # add clean list + clean_list.push id + end + end + end + + + # scan all jobs + Dir.new(jobs_path).entries.each do |id| + # skip . or .. + if id.eql? "." or id.eql? ".." then next end + + # remove directory if jobs directory is created too long ago + job_path = "#{jobs_path}/#{id}" + dir_keep_time = 86400 < @server.keep_time ? @server.keep_time : 86400 + if File.ctime(job_path) + dir_keep_time < Time.now then + FileUtils.rm_rf job_path + @server.log.info "Removed the job directory: #{id}" + next + end + + if not clean_list.include? id then + time = Time.now + @server.log.info "Registered clean-action for old job : #{id}" + @handler.register(JobCleanAction.new(time,job_path,@list_file, @server)) + end + end + + # start handler + @handler.start + end + + + # clean after some time + def clean_afterwards(job_id) + time = Time.now + @server.keep_time + job_path = "#{@server.path}/jobs/#{job_id}" + @handler.register(JobCleanAction.new(time, job_path, @list_file, @server)) + + @server.log.info "Registered delayed clean-action for the job #{job_id}" + end + + + # clean directly + def clean(job_id) + time = Time.now + job_path = "#{@server.path}/jobs/#{job_id}" + @handler.register(JobCleanAction.new(time, job_path, @list_file, @server)) + + @server.log.info "Registered clean-action for the job #{job_id}" + end +end diff --git a/src/build_server/JobLog.rb b/src/build_server/JobLog.rb index c46db48..def8056 100644 --- a/src/build_server/JobLog.rb +++ b/src/build_server/JobLog.rb @@ -1,6 +1,6 @@ =begin - - JobLog.rb + + JobLog.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -34,37 +34,80 @@ require "BuildComm.rb" class JobLog < Log - def initialize(job, path, stream_out) - super(path) + def initialize(job, stream_out, verbose = false) + log_level = (verbose) ? Log::LV_NORMAL : Log::LV_USER + if job.nil? then + super(nil) + else + if not File.exist? "#{job.server.path}/jobs/#{job.id}" then + FileUtils.mkdir_p "#{job.server.path}/jobs/#{job.id}" + end + super("#{job.server.path}/jobs/#{job.id}/log",log_level) + end @parent_job=job @second_out = stream_out end + def set_second_out( out ) + @second_out = out + end + + + def close + # close communication + if not @second_out.nil? then + begin + if not @second_out.closed? then + BuildCommServer.send_end(@second_out) + end + rescue + end + BuildCommServer.disconnect(@second_out) + end + + @second_out = nil + end + + + def is_connected? + if @second_out.nil? or @second_out.closed? then + return false + else + return true + end + end + + + def has_second_out? + if @second_out.nil? then + return false + else + return true + end + end + + protected # overide def output_extra(msg) - begin + begin if not @second_out.nil? then BuildCommServer.send( @second_out, msg ) end rescue - @parent_job.status="ERROR" - close() - error "Connection closed by remote client" + # close second_out + @second_out.close + @second_out = nil - # terminate job - @parent_job.terminate + error "Connection closed by remote client" - # exit thread if independent worker thread - if @parent_job.thread == Thread.current then - error "Thread wiil be terminated" - @parent_job.thread=nil - Thread.exit + # cancel parent job + if not @parent_job.nil? and @parent_job.cancel_state == "NONE" then + @parent_job.cancel_state = "INIT" end end - end - + end end diff --git a/src/build_server/JobManager.rb b/src/build_server/JobManager.rb new file mode 100644 index 0000000..dc6dbae --- /dev/null +++ b/src/build_server/JobManager.rb @@ -0,0 +1,589 @@ +=begin + + JobManager.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'thread' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "SocketJobRequestListener.rb" +require "RemoteBuildJob.rb" +require "RegisterPackageJob.rb" +require "packageServer.rb" + + +class JobManager + attr_accessor :jobs, :internal_jobs, :reverse_build_jobs + attr_accessor :internal_job_schedule + + # initialize + def initialize( parent ) + @server = parent + @jobs = [] + @internal_jobs = [] + @reverse_build_jobs = [] + @new_job_index = 0 + @internal_job_schedule = Mutex.new + @latest_job_touch = Mutex.new + end + + def cancel_broken_status + @server.get_db_connection() do |db| + db.do "UPDATE jobs SET status = 'CANCELED' WHERE status != 'FINISHED' and status != 'ERROR' and status != 'CANCELED'" + end + end + + def max_working_jobs + result = nil + @server.get_db_connection() do |db| + result = db.select_one("SELECT value FROM server_configs WHERE property = 'max_working_jobs'")[0] + end + return (result.nil?) ? 2 : result.to_i + end + + def max_working_jobs=(job_cnt) + @server.get_db_connection() do |db| + db.do "UPDATE server_configs SET value = '#{job_cnt}' WHERE property = 'max_working_jobs'" + end + end + + # initialize + def init() + # load latest job idx if exist + file_path = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/latest_job" + if File.exist? file_path then + latest_idx = -1 + File.open( file_path, "r" ) do |f| + f.each_line do |l| + latest_idx = l.strip.to_i + break + end + end + if latest_idx < 0 then latest_idx = -1 end + @new_job_index = latest_idx + 1 + else + @new_job_index = 0 + end + end + + + # get new id + def get_new_job_id + new_idx = 0 + @latest_job_touch.synchronize do + new_idx = @new_job_index + + file_path = "#{BuildServer::CONFIG_ROOT}/#{@server.id}/latest_job" + File.open( file_path, "w" ) do |f| + f.puts "#{@new_job_index}" + end + + @new_job_index += 1 + end + + return new_idx + end + + def is_user_accessable(job,user_id) + if job.type == "MULTIBUILD" then + job.get_sub_jobs().each do |subjob| + if is_user_accessable(subjob,user_id) then + return true + end + end + else + result = nil + @server.get_db_connection() do |db| + result = db.select_one "SELECT user_groups.group_id FROM user_groups,group_project_accesses + WHERE user_groups.group_id = group_project_accesses.group_id and + group_project_accesses.project_id = #{job.get_project.get_project_id} and + user_groups.user_id = #{user_id}" + end + return (not result.nil?) + end + return false + end + + def create_new_register_job( file_path, dist_name ) + return RegisterPackageJob.new( file_path, nil, @server, nil, dist_name ) + end + + def set_remote(job, rserver) + job.set_remote_job(rserver) + @server.get_db_connection() do |db| + db.do "UPDATE jobs SET remote_build_server_id = '#{rserver.id}' WHERE id = '#{job.id}'" + end + end + + # add a normal job + def add_job ( new_job ) + @server.log.info "Added new job \"#{new_job.id}\"" + save_job_status(new_job) + @jobs.push( new_job ) + end + + # add internal job for multi-build job + def add_internal_job( new_job ) + @server.log.info "Added new job \"#{new_job.id}\"" + save_job_status(new_job) + @internal_jobs.push( new_job ) + end + + # add reverse build chek job + def add_reverse_build_job( new_job ) + @server.log.info "Added new job \"#{new_job.id}\"" + save_job_status(new_job) + @reverse_build_jobs.push( new_job ) + end + + # stop internal job selection + def stop_internal_job_schedule() + @internal_job_schedule.lock + end + + + # start internal job selection + def resume_internal_job_schedule() + @internal_job_schedule.unlock + end + + # intialize normal job + def initialize_job ( job ) + job.status = "INITIALIZING" + job.thread = Thread.new do + save_job_status(job) + begin + # init + if not job.init or job.status == "ERROR" then + if job.cancel_state == "NONE" then job.status = "ERROR" end + @server.log.info "Adding the job \"#{job.id}\" is canceled" + job.terminate() + save_job_status(job) + Thread.current.exit + end + if job.status != "FINISHED" then + job.status = "WAITING" + save_job_status(job) + end + @server.log.info "Checking the job \"#{job.id}\" was finished!" + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + ensure + job.thread = nil + end + end + @server.log.info "Job \"#{job.id}\" entered INITIALIZING status" + end + + + #execute + def execute(job) + job.status = "WORKING" + save_job_status(job) + + # start build + job.execute + @server.log.info "Moved the job \"#{job.id}\" to working job list" + end + + + # execute remote + def execute_remote(job, rserver) + + # start build + set_remote(job, rserver) + if job.execute() then + # status change & job control + job.status = "REMOTE_WORKING" + save_job_status(job) + @server.log.info "Moved the job \"#{job.id}\" to remote job list" + else + @server.log.info "Moving the job \"#{job.id}\" to remote failed" + end + end + + def cancel_job( job) + job.cancel_state = "WORKING" + @server.log.info "Creating thread for canceling the job \"#{job.id}\"" + Thread.new do + begin + #terminate job thread + if not job.thread.nil? then + job.thread.terminate + job.thread = nil + end + + # job cacncel + job.cancel + + # cancel finished + job.status = "CANCELED" + save_job_status(job) + + # call terminate process for job + job.terminate + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + end + end + end + + # handle + def handle() + # for cancel jobs + (@jobs + @internal_jobs + @reverse_build_jobs).select {|j| j.cancel_state == "INIT" }.each do |job| + cancel_job( job ) + end + + # for reverse build jobs + job_list = @reverse_build_jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list + if job.status == "ERROR" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is stopped by ERROR" + @reverse_build_jobs.delete job + elsif job.status == "FINISHED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by FINISH status" + @reverse_build_jobs.delete job + elsif job.status == "CANCELED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by CANCELED status" + @reverse_build_jobs.delete job + end + + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + end + + # for internal jobs + job_list = @internal_jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list + if job.status == "ERROR" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is stopped by ERROR" + @internal_jobs.delete job + elsif job.status == "FINISHED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by FINISH status" + @internal_jobs.delete job + elsif job.status == "CANCELED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by CANCELED status" + @internal_jobs.delete job + end + + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + end + + # for normal job + job_list = @jobs + job_list.each do |job| + # if "ERROR", "FINISHED", "CANCELED" remove it from list + if job.status == "ERROR" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is stopped by ERROR" + @jobs.delete job + elsif job.status == "FINISHED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by FINISH status" + @jobs.delete job + elsif job.status == "CANCELED" + save_job_status(job) + @server.log.info "Job \"#{job.id}\" is removed by CANCELED status" + @jobs.delete job + end + + # if "JUST_CREATED", initialize it + if job.status == "JUST_CREATED" then + initialize_job( job ) + end + + # check the connection if job is not asynchronous job + if ( job.status == "WAITING" or job.status == "REMOTE_WORKING" or job.status == "PENDING") and + not job.is_asynchronous_job? and + not job.is_connected? then + + job.status = "ERROR" + save_job_status(job) + @jobs.delete( job ) + @server.log.info "Job \"#{job.id}\" is disconnected by user. Removed!" + end + end + + # reverse build job -> internal job -> normal job + job = get_available_job + + # available job not exist?, continue + if not job.nil? then + # oherwise, check remote server + rserver = @server.get_available_server( job ) + if rserver != nil and rserver == @server then + execute(job) + elsif rserver != nil then + execute_remote(job, rserver) + else + #puts "No available server" + end + end + + end + + + # select the job whith no build-dependency problem + def get_available_job + # select reverse build job with round-robin method + selected_job = nil + @reverse_build_jobs.each do |job| + if job.status == "WAITING" then + selected_job = job + break + end + end + # rotate array + if @reverse_build_jobs.count > 0 then + @reverse_build_jobs.push @reverse_build_jobs.shift + end + if not selected_job.nil? then return selected_job end + + # if no reverse build job exist! + @internal_job_schedule.synchronize do + # internal job first + ret = nil + if @internal_jobs.count > 0 then + ret = get_available_job_in_list(@internal_jobs, true) + end + + # not found, select normal job + if ret.nil? then + ret = get_available_job_in_list(@jobs, false) + end + + return ret + end + end + + + # return "max_working_jobs_cnt - current_working_jobs_cnt" + def get_number_of_empty_room + working_cnt = 0 + parent_list = [] + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "WORKING" then + working_cnt = working_cnt + 1 + end + + # must exclude parent job + if not job.get_parent_job().nil? then + parent_list.push job.get_parent_job() + end + end + + parent_list.uniq! + + return max_working_jobs - working_cnt + parent_list.count + end + + + # check there are working jobs + def has_working_jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "WORKING" then + return true + end + end + + return false + end + + + # check there are waiting jobs + def has_waiting_jobs + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "WAITING" then + return true + end + end + + return false + end + + + def get_working_jobs + result = [] + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "WORKING" then + result.push job + end + end + + return result + end + + + def get_waiting_jobs + result = [] + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "WAITING" then + result.push job + end + end + + return result + end + + + def get_remote_jobs + result = [] + (@jobs + @internal_jobs + @reverse_build_jobs).each do |job| + if job.status == "REMOTE_WORKING" then + result.push job + end + end + + return result + end + + + def get_pending_jobs + result = [] + @jobs.each do |job| + if job.status == "PENDING" then + result.push job + end + end + + return result + end + + def save_job_status(job) + now = @server.db_now + result = @server.get_db_connection() do |db| + job.save(db, now) + end + + return result + end + + + protected + # select the job whith no build-dependency problem + # if "check_dep_wait" is true, it will check the build dependency + # among items of "WAIT" status in list + def get_available_job_in_list( jobs, check_dep_wait=false ) + + # gather all working jobs and full-build jobs + check_dep_jobs = [] + jobs.each do |job| + if job.cancel_state != "NONE" then next end + + if job.status == "WORKING" or job.status == "REMOTE_WORKING" or job.status == "PENDING" then + check_dep_jobs.push job + elsif ( check_dep_wait and job.status == "WAITING") then + check_dep_jobs.push job + end + end + + # get candidates for waiting jobs + candidate_jobs = [] + jobs.each do |job| + if job.cancel_state != "NONE" then next end + if job.status != "WAITING" then next end + + # check build dependency against working job + pre_jobs = [] + check_dep_jobs.each do |cjob| + if job == cjob then next end + # In case that "WORKING/REMOTE_WORKING" job has build dependency on me + if (cjob.status == "WORKING" or cjob.status == "REMOTE_WORKING" ) and + (job.has_build_dependency?( cjob ) or job.is_compatible_with?( cjob)) then + pre_jobs.push cjob + # In case that "PENDING" job is depends on me (not depended ) + elsif cjob.status == "PENDING" and (not job.does_depend_on? cjob) and + (job.has_build_dependency?( cjob ) or job.is_compatible_with?( cjob)) then + pre_jobs.push cjob + elsif check_dep_wait and cjob.status == "WAITING" and + (job.does_depend_on? cjob or + (job.id > cjob.id and job.is_compatible_with? cjob) ) then + pre_jobs.push cjob + end + end + + # check pre-requisite jobs are changed, notify to user + is_changed = false + if pre_jobs.count != job.pre_jobs.count then + is_changed=true + else + pre_jobs.each do |pjob| + if not job.pre_jobs.include? pjob then + is_changed = true + break + end + end + end + if pre_jobs.count > 0 and is_changed then + job.log.info( "Waiting for finishing following jobs:", Log::LV_USER) + pre_jobs.each do |bjob| + if bjob.type == "BUILD" then + job.log.info( " * #{bjob.id} #{bjob.pkginfo.packages[0].source}", Log::LV_USER) + elsif bjob.type == "MULTIBUILD" then + job.log.info( " * #{bjob.id} (Multi Build Job)", Log::LV_USER) + end + end + end + job.pre_jobs = pre_jobs + + # no pre-requisite jobs, return its job + if job.pre_jobs.count == 0 then + candidate_jobs.push job + end + end + + # select a job by priority + if candidate_jobs.count == 0 then return nil end + max_priority = -1 + max_priority_job = nil + candidate_jobs.each do |job| + if max_priority < job.priority then + max_priority = job.priority + max_priority_job = job + end + end + + return max_priority_job + end + +end diff --git a/src/build_server/MultiBuildJob.rb b/src/build_server/MultiBuildJob.rb new file mode 100644 index 0000000..5e5d98e --- /dev/null +++ b/src/build_server/MultiBuildJob.rb @@ -0,0 +1,456 @@ +=begin + + MultiBuildJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/builder" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "client.rb" +require "PackageManifest.rb" +require "Version.rb" +require "Builder.rb" +require "RemoteBuilder.rb" +require "JobLog.rb" +require "mail.rb" +require "CommonJob.rb" + +class MultiBuildJob < CommonJob + + attr_accessor :source_path, :cancel_state + attr_accessor :pkgsvr_client, :thread + + # initialize + def initialize (server) + super(server) + @log = nil + @type = "MULTIBUILD" + + @host_os = Utils::HOST_OS + @pkgsvr_url = nil + @pkgsvr_ip = nil + @pkgsvr_port = nil + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @cancel_state = "NONE" + + end + + + def get_distribution_name() + return @sub_jobs[0].get_project().dist_name + end + + + def get_buildroot() + return @buildroot_dir + end + + + def is_rev_build_check_job() + return false + end + + def set_no_reverse() + @sub_jobs.each do |sub| + sub.no_reverse = true + end + end + + # cnacel + def cancel() + @sub_jobs.select{|x| x.cancel_state == "NONE"}.each do |sub| + sub.cancel_state = "INIT" + end + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + end + + # + def init + # mkdir + if not File.exist? @job_root then + FileUtils.mkdir_p @job_root + end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) + end + + @log.info( "Initializing job...", Log::LV_USER) + + # create source path + if not File.exist? @source_path then + FileUtils.mkdir_p @source_path + end + + # initialize all sub jobs and add them to "internal_jobs" + @sub_jobs.each do |job| + # initialize job + if not job.init or job.status == "ERROR" then + job.status = "ERROR" + @log.info( "Failed to initialize sub-job \"#{job.get_project().name}\" for #{job.os}. (#{job.id})", Log::LV_USER) + job.terminate() + end + + if job.status != "ERROR" then + job.status = "WAITING" + else + job.status = "ERROR" + @status = "ERROR" + end + end + if @status == "ERROR" then + return false + end + + + # set up pkgsvr_client + first_project = @sub_jobs[0].get_project() + @pkgsvr_url = @server.distmgr.get_distribution(first_project.dist_name).pkgsvr_url + @pkgsvr_ip = @server.distmgr.get_distribution(first_project.dist_name).pkgsvr_ip + @pkgsvr_port = @server.distmgr.get_distribution(first_project.dist_name).pkgsvr_port + @pkgsvr_client = Client.new(@pkgsvr_url, @job_working_dir, @log) + + return true + end + + + #terminate + def terminate() + + # report error + if @status == "ERROR" then + # register delayed clean action for sub jobs + @sub_jobs.each do |job| + @server.cleaner.clean_afterwards(job.id) + end + + # register delayed clean action for me + @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + + elsif @status == "CANCELED" then + # register delayed clean action for sub jobs + @sub_jobs.each do |job| + @server.cleaner.clean_afterwards(job.id) + end + + # register delayed clean action for me + @log.error( "Job is stopped by CANCEL" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + + else + # terminate all sub jobs + @sub_jobs.each do |job| + if not job.log.nil? then job.terminate() end + end + + # register direct clean action for me + @server.cleaner.clean(@id) + end + + # close logger + @log.close + end + + + # check building is possible + def can_be_built_on?(host_os) + return true + end + + + def get_packages() + packages = [] + @sub_jobs.each do |job| + packages = packages + job.get_packages() + end + packages.uniq! + + return packages + end + + + def get_build_dependencies(target_os) + deps = [] + @sub_jobs.each do |job| + deps = deps + job.get_build_dependencies(target_os) + end + deps.uniq! + + return deps + end + + + def get_source_dependencies(target_os, host_os) + deps = [] + @sub_jobs.each do |job| + deps = deps + job.get_source_dependencies(target_os,host_os) + end + deps.uniq! + + return deps + end + + + def is_compatible_with?(o) + return false + end + + def has_build_dependency?(other_job) + + if has_same_packages?(other_job) or + does_depend_on?(other_job) or + does_depended_by?(other_job) then + + return true + else + return false + end + end + + + def has_same_packages?( wjob ) + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + # same package must have same os + if not @os.eql? wjob.os then + return false + end + + # check package name + get_packages.each do |pkg| + wjob.get_packages().each do |wpkg| + if pkg.package_name == wpkg.package_name then + #puts "Removed from candiated... A == B" + return true + end + end + end + + return false + end + + + def does_depend_on?( wjob ) + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + # compare build dependency + get_build_dependencies(@os).each do |dep| + wjob.get_packages().each do |wpkg| + # dep packages of my job must have same name and target os + # with packages in working job + if dep.package_name == wpkg.package_name and + dep.target_os_list.include? wjob.os then + #puts "Removed from candiated... A -> B" + return true + end + end + end + + return false + end + + + def does_depended_by?( wjob ) + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + get_packages().each do |pkg| + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == pkg.package_name and + dep.target_os_list.include? @os then + #puts "Checking... A <- B" + return true + end + end + end + return false + end + + + def is_connected? + return true + end + + + # return the job is asyncronous job + def is_asynchronous_job? + return false + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + return "#{@server.job_log_url}/#{@id}/log","" + end + + # + # PROTECTED/PRIVATE METHODS + # + + + # main module + protected + def job_main() + @log.info( "Invoking a thread for MULTI-BUILD Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end + @log.info( "New Job #{@id} is started", Log::LV_USER) + + # initialize status map + job_status_map = {} + @sub_jobs.each do |job| + job_status_map[job.id] = job.status + end + + # add to internal job + @server.jobmgr.internal_job_schedule.synchronize do + @sub_jobs.each do |job| + # init finished, add internal_jobs + @server.jobmgr.add_internal_job(job) + @log.info( "Added new job \"#{job.get_project().name}\" for #{job.os}! (#{job.id})", + Log::LV_USER) + if not @server.job_log_url.empty? then + @log.info( " * Log URL : #{@server.job_log_url}/#{job.id}/log", Log::LV_USER) + end + end + end + + # show job status changes + all_jobs_finished = false + stop_status = "FINISHED" + while not all_jobs_finished + all_jobs_finished = true + @sub_jobs.each do |job| + # check status chanaged, if then print + if job_status_map[ job.id ] != job.status then + @log.info(" * Sub-Job \"#{job.get_project().name}(#{job.os})\" has entered \"#{job.status}\" state. (#{job.id})", Log::LV_USER) + job_status_map[ job.id ] = job.status + end + # check all jobs are finished + if job.status != "ERROR" and job.status != "FINISHED" and job.status != "CANCELED" then + all_jobs_finished = false + end + # check there is some error or cancel + if stop_status == "FINISHED" and + (job.status == "ERROR" or job.status == "CANCELED") then + # write url + write_log_url(job) + # cancel all other un-finished jobs + @sub_jobs.each do |sub| + if sub.status != "ERROR" and sub.status != "FINISHED" and + sub.status != "CANCELED" and sub.cancel_state == "NONE" then + @log.info(" * Sub-Job \"#{sub.get_project().name}(#{sub.os})\" has entered \"CANCELING\" state. (#{sub.id})", Log::LV_USER) + sub.cancel_state = "INIT" + end + end + + stop_status = job.status + break + end + end + + # + sleep 1 + end + + if stop_status == "ERROR" or stop_status == "CANCELED" then + @status = stop_status + return + end + + # upload + if not upload() then + @status = "ERROR" + return + end + + # INFO. don't change this string + @log.info( "Job is completed!", Log::LV_USER) + @status = "FINISHED" + end + + + private + def upload() + @log.info( "Uploading ...", Log::LV_USER) + + # get package path list + binpkg_path_list = Dir.glob("#{@source_path}/*_*_*.zip") + + # upload + u_client = Client.new( @pkgsvr_url, nil, @log ) + snapshot = u_client.upload( @pkgsvr_ip, @pkgsvr_port, binpkg_path_list, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + + if snapshot.nil? then + @log.info( "Upload failed...", Log::LV_USER) + + return false + end + + # update local + @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) + @pkgsvr_client.update + @log.info("Snapshot: #{snapshot}", Log::LV_USER) + + return true + end + + + # write web url for log + private + def write_log_url(job) + url,remote_url = job.get_log_url() + if not url.empty? then + @log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + @log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end + end + +end diff --git a/src/build_server/PackageSync.rb b/src/build_server/PackageSync.rb new file mode 100644 index 0000000..afc2598 --- /dev/null +++ b/src/build_server/PackageSync.rb @@ -0,0 +1,238 @@ +=begin + + PackageSync.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "Action.rb" +require "ScheduledActionHandler.rb" + + +class PackageSyncAction < Action + attr_accessor :pkgsvr_url, :dist_name + @@new_id = 0 + + def initialize( time, url, dist_name, server ) + super(time, server.pkg_sync_period) + my_id = @@new_id + @@new_id += 1 + @pkgsvr_url = url + @dist_name = dist_name + @server = server + @pkgsvr_client = nil + @main_client = nil + @sync_root = "#{@server.path}/sync/#{my_id}" + @download_path = "#{@sync_root}/remote" + @original_path = "#{@sync_root}/main" + end + + + def init + # create directory + if File.exist? @download_path then + FileUtils.rm_rf @download_path + FileUtils.rm_rf @original_path + end + + FileUtils.mkdir_p @download_path + FileUtils.mkdir_p @original_path + + # create client + @pkgsvr_client = Client.new( @pkgsvr_url, @download_path, @server.log ) + + main_url = @server.distmgr.get_distribution(@dist_name).pkgsvr_url + @main_client = Client.new( main_url, @original_path, @server.log ) + end + + + def execute + @server.log.info "Executing package-sync action for server \"#{@pkgsvr_url}\"" + + begin + execute_internal() + rescue => e + @server.log.error e.message + @server.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # check update + pkgs = check_package_update + + # request to register + registered_jobs = [] + + # if updates are found, download them + downloaded_files = [] + pkgs.each do |pkg| + pkg_name=pkg[0]; os=pkg[1]; prj=pkg[2] + + file_paths = @pkgsvr_client.download(pkg_name, os, false) + if file_paths.nil? then next end + downloaded_files += file_paths + + file_paths.each do |file_path| + @server.log.info "Creating new job for registering \"#{file_path}\"" + new_job = @server.jobmgr.create_new_register_job( file_path, @dist_name ) + if new_job.nil? then + @server.log.error "Creating job failed: #{prj.name} #{pkg_name} #{@dist_name}" + next + end + new_job.priority = CommonJob::AUTO_JOB_PRIORITY + new_job.create_logger( nil ) + + # add + @server.jobmgr.add_job( new_job ) + registered_jobs.push new_job + end + end + + # wait for finish all jobs + all_jobs_finished = false + while not all_jobs_finished + unfinished_jobs = registered_jobs.select do |j| + (j.status != "ERROR" and j.status != "FINISHED" and j.status != "CANCELED") + end + if unfinished_jobs.empty? then + all_jobs_finished = true + else + sleep 10 + end + end + + # remove files + downloaded_files.each do |file_path| + @server.log.info "Removed downloaded file: \"#{file_path}\"" + FileUtils.rm_rf file_path + end + end + + + protected + def check_package_update + pkgs = [] + + # update + @pkgsvr_client.update() + @main_client.update() + + # for all BINARY project + bin_prjs = @server.prjmgr.get_all_projects().select { |p| + (p.type == "BINARY" and p.dist_name == @dist_name) + } + bin_prjs.each do |p| + pkg_name = p.pkg_name + p.os_list.each do |os| + # get pkg version in server + main_ver = @main_client.get_attr_from_pkg(pkg_name, os, "version") + remote_ver = @pkgsvr_client.get_attr_from_pkg(pkg_name, os, "version") + if remote_ver.nil? then next end + + if main_ver.nil? or Version.new(main_ver) < Version.new(remote_ver) then + pkgs.push [pkg_name, os, p] + end + end + end + + return pkgs + end + +end + + +class PackageServerSynchronizer + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + end + + + # start thread + def start() + + # start thread for handling action + @handler.start + + Thread.new do + monitor() + end + end + + + private + def monitor() + while(true) + # wait 10 seconds + sleep 10 + + # get info from DB + syncs = @server.get_sync_package_servers() + + # check removal + @handler.get_actions().each do |act| + exist = false + syncs.each do |sync| + url=sync[0]; dist_name=sync[1] + if url == act.pkgsvr_url and dist_name == act.dist_name then + exist = true + break + end + end + if not exist then + @handler.unregister(act) + @server.log.info "Unregistered package-sync action!: #{act.dist_name} <= \"#{act.pkgsvr_url}\"" + end + end + + # check add/modify + syncs.each do |sync| + url=sync[0]; dist_name=sync[1] + exist = false + @handler.get_actions().each do |act| + if act.pkgsvr_url == url and act.dist_name == dist_name then + exist = true + end + end + + if not exist then + new_time = Time.new + 10 + @handler.register( PackageSyncAction.new(new_time, url, dist_name, @server) ) + @server.log.info "Registered package-sync action!: #{dist_name} <= \"#{url}\"" + end + end + + end + end +end diff --git a/src/build_server/ProjectManager.rb b/src/build_server/ProjectManager.rb new file mode 100644 index 0000000..c717213 --- /dev/null +++ b/src/build_server/ProjectManager.rb @@ -0,0 +1,364 @@ +=begin + + ProjectManager.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'dbi' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "GitBuildProject.rb" +require "BinaryUploadProject.rb" +require "MultiBuildJob.rb" +require "PackageManifest.rb" +require "package.rb" + +class ProjectManager + + # initialize + def initialize( server ) + @server = server + @project_root = "#{@server.path}/projects" + end + + + # load existing project from server configuration + def load() + # check project root + if not File.exist? @project_root then + FileUtils.mkdir_p @project_root + end + end + + + # get_project of the name + def get_project(name, dist_name) + @server.get_db_connection() do |db| + return get_project_internal(name, dist_name, db) + end + + return nil + end + + + def get_all_projects_summary() + @server.get_db_connection() do |db| + return CommonProject.get_all_project_rows(db) + end + end + + def get_all_projects() + result = [] + + @server.get_db_connection() do |db| + rows = CommonProject.get_all_project_rows(db) + rows.each do |row| + if row[:ptype] == "GIT" then + prj = GitBuildProject.load(row[:name], row[:dist_name], @server, db) + else + prj = BinaryUploadProject.load(row[:name], row[:dist_name], @server, db) + end + if not prj.nil? then result.push prj end + end + return result + end + + return result + end + + + def add_git_project(name, repos, branch, passwd, os_list, dist_name) + new_prj = nil + result = @server.get_db_connection() do |db| + prj = get_project_internal(name, dist_name, db) + if not prj.nil? then + @server.log.error "Adding project failed!: the project \"#{name}\"(#{dist_name}) already exists" + return false + end + + # create new object + new_prj = GitBuildProject.new(name, @server, os_list, dist_name, repos, branch) + if not passwd.nil? and not passwd.empty? then + new_prj.passwd = passwd + end + + # save to db + new_prj.save(db) + end + + if result then + # authorize admin to access + @server.qualify_admin_to_access(new_prj.get_project_id()) + @server.log.info "Added new GIT project \"#{name}\"(#{dist_name})" + end + + return result + end + + + def add_binary_project(name, pkg_name, passwd, os_list, dist_name) + new_prj = nil + result = @server.get_db_connection() do |db| + prj = get_project_internal(name, dist_name, db) + if not prj.nil? then + @server.log.error "Adding project failed!: the project \"#{name}\"(#{dist_name}) already exists" + return false + end + + # create new object + new_prj = BinaryUploadProject.new(name, @server, os_list, dist_name, pkg_name) + if not passwd.nil? and not passwd.empty? then + new_prj.passwd = passwd + end + + # save to db + new_prj.save(db) + + # init + new_prj.init() + end + + if result then + # authorize admin to access + @server.qualify_admin_to_access(new_prj.get_project_id()) + @server.log.info "Added new BINARY project \"#{name}\"(#{dist_name})" + end + + return result + end + + + def remove_project( name, dist_name ) + + result = @server.get_db_connection() do |db| + prj = get_project_internal(name, dist_name, db) + if prj.nil? then + @server.log.error "The project \"#{name}\"(#{dist_name}) does not exists on server" + @server.log.error "Removing project failed!" + return false + end + # unload from DB + prj.unload(db) + + # remove project directory + FileUtils.rm_rf prj.path + end + + if result then + @server.log.info "Removed the project \"#{name}\"(#{dist_name})" + end + return result + end + + public + def get_project_accessable(project_name, dist_name, user_id) + prj = get_project(project_name, dist_name) + result = nil + @server.get_db_connection() do |db| + result = db.select_one "SELECT user_groups.group_id FROM user_groups,group_project_accesses + WHERE user_groups.user_id = #{user_id} and + user_groups.group_id = group_project_accesses.group_id and + group_project_accesses.project_id = #{prj.get_project_id} and + group_project_accesses.build = 'TRUE'" + end + return (not result.nil?) + end + + def get_project_pkg_name_accessable(pkg_name, dist_name, user_id) + result = nil + prj = get_project_from_package_name(pkg_name, dist_name) + @server.get_db_connection() do |db| + result = db.select_one "SELECT user_groups.group_id FROM user_groups,group_project_accesses + WHERE user_groups.user_id = #{user_id} and + user_groups.group_id = group_project_accesses.group_id and + group_project_accesses.project_id = #{prj.get_project_id} and + group_project_accesses.build = 'TRUE'" + end + return (not result.nil?) + end + + # create new job for project + # if cannot create, return nil + def create_new_job( name, os, dist_name ) + prj = get_project( name, dist_name ) + if prj.nil? then + @server.log.error "Cannot get project info \"#{name}\"(#{dist_name})" + end + + return prj.create_new_job( os ) + end + + + # create new multi build job + def create_new_multi_build_job( sub_job_list ) + result = MultiBuildJob.new( @server ) + + sub_job_list.each do |job| + result.add_sub_job( job ) + end + + return result + end + + + # create new full job + def create_new_full_build_job(dist_name="BASE") + # create multi job + result = MultiBuildJob.new( @server ) + + # create sub jobs + get_all_projects().each do |prj| + if prj.type != "GIT" then next end + if prj.dist_name != dist_name then next end + + prj.os_list.each do |os| + if not @server.supported_os_list.include? os then next end + + new_job = create_new_job( prj.name, os, dist_name ) + if new_job.nil? then next end + + # This make project to build + # even though there is a package of same version on pkg-server + new_job.set_force_rebuild(true) + + # add to multi job + result.add_sub_job( new_job ) + end + end + + return result + end + + + # get project that includes specified pkg name and os + # will return [project,os,ver] list + def get_projects_from_pkgs(pkgs, dist_name="BASE") + result = [] + get_all_projects().each do |prj| + # check distribution name + if prj.dist_name != dist_name then next end + + pkgs.each do |pkg| + name = pkg.package_name + ver = pkg.version + os = pkg.os + + # check project provide target package + if prj.include_package?(name, ver, os) then + result.push [prj, os, ver] + break + end + end + end + + return result + end + + + def get_project_from_package_name(pkg_name, dist_name) + @server.get_db_connection() do |db| + return get_project_from_pkg_name_internal(pkg_name,dist_name,db) + end + return nil + end + + + # get project from git repository + def get_git_project( repos, dist_name ) + get_all_projects().each do |prj| + # check project's distribution + if prj.dist_name != dist_name then next end + if prj.type == "GIT" and prj.repository == repos then + return prj + end + end + + return nil + end + + + def create_unnamed_git_project(repos, dist_name) + name = "UNNAMED_PRJ_#{get_all_projects().count}" + branch = "master" + passwd = nil + os_list = Utils.get_all_OSs() + os_list.each do |os| + @server.add_supported_os(os) + end + # add + add_git_project(name , repos, branch, passwd, os_list, dist_name) + # get + return get_project(name, dist_name) + end + + + # write configuration + def write_configuration(name, repos, branch, passwd, os_list ) + config_file = "#{@project_root}/#{name}/build" + File.open( config_file, "w" ) do |f| + f.puts "TYPE=GIT" + if not passwd.nil? and not passwd.empty? then + f.puts "PASSWD=#{passwd}" + end + f.puts "GIT_REPOSITORY=#{repos}" + f.puts "GIT_BRANCH=#{branch}" + f.puts "OS_LIST=#{os_list.join(",")}" + end + end + + + # write configuration + def write_configuration_for_binary_project(name, pkg_name, passwd, os_list ) + config_file = "#{@project_root}/#{name}/build" + File.open( config_file, "w" ) do |f| + f.puts "TYPE=BINARY" + if not passwd.nil? and not passwd.empty? then + f.puts "PASSWD=#{passwd}" + end + f.puts "PACKAGE_NAME=#{pkg_name}" + f.puts "OS_LIST=#{os_list.join(",")}" + end + end + + + private + def get_project_internal(name, dist_name, db) + row = CommonProject.get_project_row(name, dist_name, db) + if row.nil? then return nil end + prj_type = row['ptype'] + + if prj_type == "GIT" then + return GitBuildProject.load(name, dist_name, @server, db) + else + return BinaryUploadProject.load(name, dist_name, @server, db) + end + end + + private + def get_project_from_pkg_name_internal(pkg_name, dist_name, db) + row = CommonProject.get_project_from_pkg_name_row(pkg_name, dist_name, db) + return ( row.nil? ) ? nil : BinaryUploadProject.load(row[:name], dist_name, @server, db) + end +end diff --git a/src/build_server/RegisterPackageJob.rb b/src/build_server/RegisterPackageJob.rb new file mode 100644 index 0000000..b745a3c --- /dev/null +++ b/src/build_server/RegisterPackageJob.rb @@ -0,0 +1,549 @@ +=begin + + RegisterBinaryJob.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/builder" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/plkg_server" +require "client.rb" +require "PackageManifest.rb" +require "Version.rb" +require "JobLog.rb" +require "mail.rb" +require "utils.rb" +require "ReverseBuildChecker.rb" +require "CommonJob.rb" + +class RegisterPackageJob < CommonJob + + attr_accessor :source_path + attr_accessor :pkgsvr_client, :thread, :pkg_type + attr_accessor :pkg_name, :pkginfo, :cancel_state + attr_accessor :no_reverse + + + # initialize + def initialize( local_path, project, server, ftpurl=nil, dist_name=nil ) + super(server) + @log = nil + @type = "REGISTER" + @no_reverse = false + + @host_os = Utils::HOST_OS + if not project.nil? then + @pkgsvr_url = @server.distmgr.get_distribution(project.dist_name).pkgsvr_url + @pkgsvr_ip = @server.distmgr.get_distribution(project.dist_name).pkgsvr_ip + @pkgsvr_port = @server.distmgr.get_distribution(project.dist_name).pkgsvr_port + else + @pkgsvr_url = @server.distmgr.get_distribution(dist_name).pkgsvr_url + @pkgsvr_ip = @server.distmgr.get_distribution(dist_name).pkgsvr_ip + @pkgsvr_port = @server.distmgr.get_distribution(dist_name).pkgsvr_port + end + @job_root = "#{@server.path}/jobs/#{@id}" + @source_path = @job_root+"/temp" + @job_working_dir=@job_root+"/works" + @buildroot_dir = "#{@job_root}/buildroot" + @cancel_state = "NONE" + + @local_path=local_path + @file_path = nil + @filename = File.basename(local_path) + if @filename =~ /.*_.*_.*\.zip/ then + @pkg_type = "BINARY" + new_name = @filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + @pkg_name = new_name.split(",")[0] + @pkg_version = new_name.split(",")[1] + @os = new_name.split(",")[2] + else + @pkg_type = "ARCHIVE" + @pkg_name = @filename + end + @pkginfo = nil #This info is valid only for BINARY package + @project = project + if not dist_name.nil? then + @dist_name = dist_name + elsif not @project.nil? then + @dist_name = @project.dist_name + else + @dist_name = "BASE" + end + @auto_remove = false + end + + + def get_distribution_name() + return @dist_name + end + + + def get_buildroot() + return @buildroot_dir + end + + + def is_rev_build_check_job() + return false + end + + + def set_auto_remove(value) + @auto_remove=value + end + + + def set_no_reverse() + @no_reverse = true + end + + + # + def init + # mkdir + if not File.exist? @job_root then + FileUtils.mkdir_p @job_root + end + + if @cancel_state != "NONE" then return false end + + # create logger + if @log.nil? then + @log = JobLog.new(self, nil ) + end + + if @cancel_state != "NONE" then return false end + + @log.info( "Initializing job...", Log::LV_USER) + + # create dummy source path + if not File.exist? @source_path then + FileUtils.mkdir_p @source_path + end + + # copy package file to source path + @file_path = "#{@source_path}/#{File.basename(@local_path)}" + if not File.exist? @local_path then + @log.error( "File not found!", Log::LV_USER) + @status = "ERROR" + return false + else + if @auto_remove then + # if remote upload remove file and its directory + FileUtils.mv(@local_path, @file_path) + FileUtils.rm_rf("#{File.dirname(@local_path)}") + else + FileUtils.cp(@local_path, @file_path) + end + end + + if @cancel_state != "NONE" then return false end + + # set up pkgsvr_client + @pkgsvr_client = Client.new(@pkgsvr_url, @job_working_dir, @log) + + if @cancel_state != "NONE" then return false end + + # check if the os is supported by build server + if @pkg_type == "BINARY" and + not @server.supported_os_list.include? @os then + @log.error( "Unsupported OS \"#{@os}\" is used!", Log::LV_USER) + @status = "ERROR" + return false + end + + if @cancel_state != "NONE" then return false end + + # checking version if not reverse-build job + if @pkg_type == "BINARY" then + # extrac pkg file + cmd = "cd \"#{@source_path}\";unzip #{@file_path}" + if not Utils.execute_shell(cmd) then + @log.error( "Extracting package file failed!", Log::LV_USER) + @status = "ERROR" + return false + end + + if @cancel_state != "NONE" then return false end + + # set up pkg info + begin + @pkginfo = PackageManifest.new("#{@source_path}/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + @status = "ERROR" + return false + end + + #if @server.changelog_check and not @pkginfo.packages[0].does_change_exist? then + # @log.error( "change log not found", Log::LV_USER ) + # return false + #end + + if @cancel_state != "NONE" then return false end + + if not check_package_version() then + @status = "ERROR" + return false + end + end + + if @cancel_state != "NONE" then return false end + + return true + end + + + #terminate + def terminate() + # report error + if @status == "ERROR" then + @log.error( "Job is stopped by ERROR" , Log::LV_USER) + @server.cleaner.clean_afterwards(@id) + else + @log.info( "Job is FINISHED successfully!" , Log::LV_USER) + + # if succeeded, register source info and copy pkginfo.manifest + if not @project.nil? then + @log.info( "Updating the source info for project \"#{@project.name}\"" , Log::LV_USER) + @project.save_source_info( @pkginfo.get_version(), '') + @project.save_package_info_from_manifest( @pkginfo.get_version(),"#{@source_path}/pkginfo.manifest", @os) + end + + # clean up + @server.cleaner.clean(@id) + end + + # close logger + @log.close + end + + + #cancel + def cancel() + if not @log.nil? then + @log.info( "JOB is canceled by cancel operation !!", Log::LV_USER) + end + end + + + # check building is possible + def can_be_built_on?(host_os) + return true + end + + + def get_packages() + if @pkg_type == "BINARY" then + return @pkginfo.packages + else + return [] + end + end + + + def get_build_dependencies(target_os) + return [] + end + + + def get_source_dependencies(target_os,host_os) + return [] + end + + + def is_compatible_with?(o) + return false + end + + + def has_build_dependency?(other_job) + if has_same_packages?(other_job) or + does_depended_by?(other_job) then + + return true + else + return false + end + end + + + def has_same_packages?( wjob ) + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + if @type != wjob.type then return false end + + case @pkg_type + when "BINARY" + if @pkg_name == wjob.pkg_name and + @os == wjob.os then + return true + end + when "ARCHIVE" + if @pkg_name == wjob.pkg_name then return true end + end + + return false + end + + + # binary/archive package should not have build-dependencies + def does_depend_on?( wjob ) + return false + end + + + def does_depended_by?( wjob ) + # must have same distribution + if get_distribution_name() != wjob.get_distribution_name() then + return false + end + + if @pkg_type == "BINARY" then + wjob.get_build_dependencies(wjob.os).each do |dep| + # dep package of working job must have same name and target os + # with packages in my job + if dep.package_name == @pkg_name and + dep.target_os_list.include? @os then + #puts "Checking... A <- B" + return true + end + end + else + wjob.get_source_dependencies(wjob.os,@host_os).each do |dep| + if dep.package_name == @pkg_name then + return true + end + end + end + + return false + end + + + def is_connected? + return true + end + + + # return the job is asyncronous job + def is_asynchronous_job? + return false + end + + + def progress + return "" + end + + + def get_log_url() + # only when server support log url + if @server.job_log_url.empty? then + return "","" + end + + return "#{@server.job_log_url}/#{@id}/log","" + end + + # + # PROTECTED METHODS + # + + + # main module + protected + def job_main() + @log.info( "Invoking a thread for REGISTER Job #{@id}", Log::LV_USER) + if @status == "ERROR" then return end + @log.info( "New Job #{@id} is started", Log::LV_USER) + + # clean build + if not ReverseBuildChecker.check( self, true ).empty? then + @status = "ERROR" + @log.error( "Reverse-build-check failed!" ) + return + end + + # if this package has compatible OS, check + if @pkg_type == "BINARY" and + @pkginfo.packages[0].os_list.count > 1 then + + pkg = @pkginfo.packages[0] + pkg.os_list.each do |os| + if @os == os then next end + + # skip when the os does not exist in project's supported os list + if not @project.nil? and not @project.os_list.include? os then next end + + # skip when there is higher version of the package + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + if not ver_svr.nil? and + Version.new(@pkg_version) <= Version.new(ver_svr) then next end + + # make new package file for compatible OS + newfile = "#{@pkg_name}_#{@pkg_version}_#{os}.zip" + @log.info( "Copying #{@filename} to #{newfile}" ) + FileUtils.cp(@file_path,"#{@source_path}/#{newfile}") + + # reverse check + if not @no_reverse then + if not ReverseBuildChecker.check( self, true, os ) then + @status = "ERROR" + @log.error( "Reverse-build-check failed!" ) + return + end + end + end + end + + # upload + if not upload() then + @status = "ERROR" + return + end + + # INFO. don't change this string + @log.info( "Job is completed!", Log::LV_USER) + @status = "FINISHED" + end + + + # build projects that dependes on me + # can ignore some projects + def check_reverse_build( target_os ) + @log.info( "Checking reverse build dependency ...", Log::LV_USER) + + # get reverse-dependent projects + rev_pkgs = [] + if @pkg_type == "BINARY" then + rev_pkgs += @pkgsvr_client.get_reverse_build_dependent_packages(@pkg_name, target_os) + else + rev_pkgs += @pkgsvr_client.get_reverse_source_dependent_packages(@pkg_name) + end + + rev_projects = @server.prjmgr.get_projects_from_pkgs(rev_pkgs, get_distribution_name()) + + # create reverse build job + rev_build_jobs = [] + rev_projects.each do |p| + prj = p[0] + os = p[1] + version = p[2] + + if prj.type != "GIT" then next end + + # create sub jobs for checking + new_job = prj.create_new_job_from_version(os, version) + new_job.set_rev_build_check_job(self) + + rev_build_jobs.push new_job + end + + # reverse build + if rev_build_jobs.count > 0 then + rev_prjs_txt = rev_build_jobs.map {|j| "#{j.get_project().name}(#{j.os})"}.join(", ") + @log.info( " * Will check reverse-build for next projects: #{rev_prjs_txt}", Log::LV_USER) + end + rev_build_jobs.each do |new_job| + @log.info( " * Checking reverse-build ... #{new_job.get_project().name}(#{new_job.id})", Log::LV_USER) + # job init + result = new_job.init() + # if init is succeeded!, try to execute + if result then + # check available server + rserver = @server.get_available_server( new_job ) + if rserver != nil and rserver != @server then + new_job.set_remote_job( rserver ) + end + # execute + new_job.execute(true) + if new_job.status == "ERROR" then result = false end + end + + # check result + if not result then + return false + end + end + + return true + end + + + def upload() + @log.info( "Uploading ...", Log::LV_USER) + + # get package path list + if @pkg_type == "ARCHIVE" then + binpkg_path_list = Dir.glob("#{@source_path}/#{@pkg_name}") + else + binpkg_path_list = Dir.glob("#{@source_path}/*_*_*.zip") + end + + # upload + u_client = Client.new( @pkgsvr_url, nil, @log ) + snapshot = u_client.upload( @pkgsvr_ip, @pkgsvr_port, binpkg_path_list, @server.ftp_addr, @server.ftp_port, @server.ftp_username, @server.ftp_passwd) + + if snapshot.nil? then + @log.info( "Upload failed...", Log::LV_USER) + + return false + end + + # update local + @log.info( "Upload succeeded. Sync local pkg-server again...", Log::LV_USER) + @pkgsvr_client.update + @log.info("Snapshot: #{snapshot}", Log::LV_USER) + + return true + end + + + # check if local package version is greater than server + def check_package_version() + @log.info( "Checking package version ...", Log::LV_USER) + + # package update + @pkgsvr_client.update + + @pkginfo.packages.each do |pkg| + ver_local = pkg.version + #ver_svr = @pkgsvr_client.get_package_version( pkg.package_name, @os ) + ver_svr = @pkgsvr_client.get_attr_from_pkg( pkg.package_name, @os, "version") + if not ver_svr.nil? and Version.new(ver_local) <= Version.new(ver_svr) then + @log.error( "Version must be increased : #{ver_local} <= #{ver_svr}", Log::LV_USER) + return false + end + end + + return true + end +end diff --git a/src/build_server/RemoteBuildJob.rb b/src/build_server/RemoteBuildJob.rb index 8e9355b..4645786 100644 --- a/src/build_server/RemoteBuildJob.rb +++ b/src/build_server/RemoteBuildJob.rb @@ -1,5 +1,5 @@ =begin - + RemoteBuildJob.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -31,14 +31,15 @@ $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "BuildJob.rb" require "utils.rb" + class RemoteBuildJob < BuildJob attr_accessor :id # initialize - def initialize (id) - super() + def initialize (id,server) + super(nil,nil,server) + # overide id @id = id @type = nil - @outstream = nil end end diff --git a/src/build_server/RemoteBuildServer.rb b/src/build_server/RemoteBuildServer.rb new file mode 100644 index 0000000..b038be1 --- /dev/null +++ b/src/build_server/RemoteBuildServer.rb @@ -0,0 +1,248 @@ +=begin + + RemoteBuildServer.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +$LOAD_PATH.unshift File.dirname(__FILE__) +require "RemoteBuildJob.rb" +require "BuildComm.rb" +require 'thread' + +class RemoteBuildServer + attr_accessor :id, :ip, :port, :description, :status, :host_os + attr_accessor :max_working_jobs, :working_jobs, :waiting_jobs, :working_job_count, :waiting_job_count + attr_accessor :path + attr_accessor :jobmgr, :distmgr + + # initialize + def initialize(ip, port, desc) + @id = -1 + @ip = ip + @port = port + @description = desc + @status = "DISCONNECTED" + @host_os = Utils::HOST_OS + @max_working_jobs = 2 + @working_jobs = [] + @working_job_count = 0 + @waiting_jobs = [] + @waiting_job_count = 0 + @path = "" + @file_transfer_cnt_mutex = Mutex.new + @file_transfer_cnt = 0 + @jobmgr = nil + @distmgr = nil + end + + + # check the job can be built on this server + def can_build?(job) + + # check me + if job.can_be_built_on? @host_os then + return true + end + + return false + end + + + # query remote server info & update server state + def update_state(db) + + # send + #@status = "DISCONNECTED" + client = BuildCommClient.create( @ip, @port ) + if client.nil? then + @status = "DISCONNECTED" + db.do "UPDATE remote_build_servers SET status = 'DISCONNECTED', max_job_count = 0, working_job_count = 0, waiting_job_count = 0 WHERE id = #{@id}" + return + end + if client.send("QUERY|SYSTEM") then + result = client.read_lines do |l| + tok = l.split(",").map { |x| x.strip } + @host_os = tok[0] + @max_working_jobs = tok[1].to_i + @status = "RUNNING" + end + if not result then @status = "DISCONNECTED" end + else + @status = "DISCONNECTED" + end + client.terminate + if @status == "DISCONNECTED" then + db.do "UPDATE remote_build_servers SET status = 'DISCONNECTED', max_job_count = 0, working_job_count = 0, waiting_job_count = 0 WHERE id = #{@id}" + return + end + + # send + @working_jobs = [] + @waiting_jobs = [] + client = BuildCommClient.create( @ip, @port ) + if client.nil? then + @status = "DISCONNECTED" + db.do "UPDATE remote_build_servers SET status = 'DISCONNECTED', max_job_count = 0, working_job_count = 0, waiting_job_count = 0 WHERE id = #{@id}" + return + end + if client.send("QUERY|JOB") then + result = client.read_lines do |l| + tok = l.split(",").map { |x| x.strip } + + job_status = tok[0] + job_id = tok[1] + new_job = RemoteBuildJob.new(job_id,self) + case job_status + when "WAITING", "JUST_CREATED", "INITIALIZING" + @waiting_jobs.push new_job + when "WORKING" + @working_jobs.push new_job + else + #puts "Uncontrolled status" + end + end + if not result then @status = "DISCONNECTED" end + else + @status = "DISCONNECTED" + end + client.terminate + if @status == "DISCONNECTED" then + db.do "UPDATE remote_build_servers SET status = 'DISCONNECTED', max_job_count = 0, working_job_count = 0, waiting_job_count = 0 WHERE id = #{@id}" + else + @working_job_count = @working_jobs.count + @waiting_job_count = @waiting_jobs.count + db.do "UPDATE remote_build_servers SET + status = '#{@status}', + supported_os_id = (SELECT supported_os.id FROM supported_os WHERE supported_os.name = '#{@host_os}'), + max_job_count = #{@max_working_jobs}, + working_job_count = #{@working_job_count}, + waiting_job_count = #{@waiting_job_count} WHERE id = #{@id}" + end + end + + + # return available working slot + def get_number_of_empty_room + return @max_working_jobs - @working_job_count + end + + + # check there are working jobs + def has_working_jobs + return (@working_job_count > 0) + end + + + # check there are waiting jobs + def has_waiting_jobs + return (@waiting_job_count > 0) + end + + + def add_file_transfer() + @file_transfer_cnt_mutex.synchronize do + @file_transfer_cnt += 1 + end + end + + def remove_file_transfer() + @file_transfer_cnt_mutex.synchronize do + @file_transfer_cnt -= 1 + end + end + + def get_file_transfer_cnt() + return @file_transfer_cnt + end + + + def set_id(id) + @id = id + end + + + def self.create_table(db, inc, post_fix) + db.do "CREATE TABLE remote_build_servers ( + id INTEGER PRIMARY KEY #{inc}, + svr_addr VARCHAR(64) NOT NULL UNIQUE, + description VARCHAR(256), + status VARCHAR(32), + supported_os_id INTEGER, + max_job_count INTEGER, + working_job_count INTEGER, + waiting_job_count INTEGER, + CONSTRAINT fk_remote_build_servers_supported_os1 FOREIGN KEY ( supported_os_id ) REFERENCES supported_os ( id ) )#{post_fix}" + end + + + def self.load(ip, port, db) + saddr="#{ip}:#{port}" + row = db.select_one("SELECT remote_build_servers.*,supported_os.name as host_os_name FROM remote_build_servers, supported_os WHERE svr_addr='#{saddr}' and remote_build_servers.supported_os_id = supported_os.id") + if not row.nil? then + return load_row(row) + end + + return nil + end + + + def self.load_all(db) + result = [] + rows = db.select_all("SELECT *,'' as host_os_name FROM remote_build_servers WHERE supported_os_id IS NULL + UNION ALL + SELECT remote_build_servers.*, supported_os.name as host_os_name FROM remote_build_servers, supported_os WHERE remote_build_servers.supported_os_id = supported_os.id") + rows.each do |row| + result.push load_row(row) + end + + return result + end + + + def self.load_row(row) + svr_ip,svr_port=row['svr_addr'].strip.split(":") + new_obj = new(svr_ip, svr_port, row['description'] ) + new_obj.set_id( row['id'] ) + new_obj.status = row['status'] + new_obj.max_working_jobs =row['max_job_count'] + new_obj.working_job_count =row['working_job_count'] + new_obj.waiting_job_count =row['waiting_job_count'] + new_obj.host_os = row['host_os_name'] + return new_obj + end + + + def unload(db) + db.do("DELETE FROM remote_build_servers WHERE id=#{@id}") + end + + + def save(db) + saddr="#{@ip}:#{@port}" + db.do "INSERT INTO remote_build_servers (svr_addr,description) VALUES ('#{saddr}','#{@description}')" + end +end + diff --git a/src/build_server/RemoteBuilder.rb b/src/build_server/RemoteBuilder.rb new file mode 100644 index 0000000..78d3fa9 --- /dev/null +++ b/src/build_server/RemoteBuilder.rb @@ -0,0 +1,259 @@ +=begin + + RemoteBuilder.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "utils" +require "PackageManifest" +require "log" +require "FileTransferViaFTP" +require "FileTransferViaDirect" + +class RemoteBuilder + attr_accessor :id, :log + + # initialize + def initialize( id, server,ftp_addr, ftp_port, ftp_username, ftp_passwd) + @id = id + @server = server + @addr = server.ip + @port = server.port + @ftp_addr = ftp_addr + @ftp_port = ftp_port + @ftp_username = ftp_username + @ftp_passwd = ftp_passwd + @log = DummyLog.new + @job = nil + end + + + # build_job + def build_job( job, local_pkgs ) + # set job + @job = job + old_log = @log + @log = job.log + + # build + ret = build(@job.get_project().repository, @job.source_path, @job.os, + @job.is_rev_build_check_job(), @job.git_commit, @job.no_reverse, + local_pkgs, @job.get_project().dist_name,"admin@user" ) + + # reset job + @job = nil + @log = old_log + + # return + return ret + end + + + # build + def build( git_repos, source_path, os, is_rev_build, srcinfo, no_reverse, local_pkgs, dist_name, user_email ) + @log.info( "Start to build on remote server...", Log::LV_USER ) + + # create unique dock number + dock = Utils.create_uniq_name() + + # send local packages + begin + @server.add_file_transfer() + local_pkgs.each do |pkg_path| + @log.info( "Sending file... : #{pkg_path}", Log::LV_USER ) + result = send_file_to_remote( pkg_path, dock ) + if not result then + @log.error( "File transfering failed!", Log::LV_USER ) + @server.remove_file_transfer() + return false + end + end + ensure + @server.remove_file_transfer() + end + + # send build request + @log.info( "Sending build request to remote server...", Log::LV_USER ) + result, result_files = send_build_request(git_repos, os, is_rev_build, + srcinfo, no_reverse, local_pkgs, dock, dist_name, user_email, @log.is_verbose) + + @log.info( "Receiving log file from remote server...", Log::LV_USER ) + if not receive_file_from_remote( "#{source_path}/../remote_log", dock ) then + @log.warn( "File transfering failed! : remote_log", Log::LV_USER ) + end + + if not result then + @log.error( "Building job on remote server failed!", Log::LV_USER ) + return false + end + + # receive binary package + result_files.each do |file_name| + @log.info( "Receiving file from remote server : #{file_name}", Log::LV_USER ) + result = receive_file_from_remote( "#{source_path}/#{file_name}", dock ) + if not result then + @log.error( "File transfering failed! : #{file_name}", Log::LV_USER ) + return false + end + end + + return true + end + + + # upload binary packages that is need to be overwrite + # before remote package + protected + def send_file_to_remote(file_path, dock = "0") + # create client + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false + end + + # upload file + result = true + file_name = file_path.split("/")[-1] + msg = "UPLOAD|#{dock}" + if client.send( msg ) then + if not @ftp_addr.nil? then + transporter=FileTransferFTP.new(@log, @ftp_addr, @ftp_port, @ftp_username, @ftp_passwd) + else + transporter=FileTransferDirect.new(@log) + end + + result=client.send_file( file_path, transporter ) + if not result then + @log.error( "File uploading failed...#{file_name}", Log::LV_USER) + end + end + + #close connections + client.terminate + + return result + end + + + # send build request + protected + def send_build_request(git_repos, os, is_rev_build, commit, no_reverse, local_pkgs, dock, dist_name, user_email, verbose) + result_files = [] + + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false, result_files + end + + # get local package names + local_pkg_names = local_pkgs.map { |path| File.basename(path) } + + # send + # 0 | 1 | 2 | 3 | 4| 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 + # format: BUILD|GIT|repository|passwd|os|async|no_reverse|dist_name|user_email|verbose|internal|rev-build|commit|pkgs|dock_num + # value : BUILD|GIT|repository| |os|NO |no_reverse|dist_name|user_email|verbose|YES |rev-build|commit|pkgs|dock_num + result = true + commit = commit.nil? ? "":commit + pkg_list = local_pkg_names.join(",") + rev = is_rev_build ? "YES":"NO" + msg = "BUILD|GIT|#{git_repos}||#{os}|NO|#{no_reverse}|#{dist_name}|#{user_email}|#{verbose}|YES|#{rev}|#{commit}|#{pkg_list}|#{dock}" + result = client.send( msg ) + if not result then + @log.error( "Communication failed! #{client.get_error_msg()}", Log::LV_USER) + return false, result_files + end + + r_job_number = Regexp.new('Added new job "([^"]*)"') + error = false + result = client.read_lines do |l| + # write log first + @log.output( l.strip, Log::LV_USER) + + # set remote job id + if not @job.nil? and @job.remote_id.nil? and l =~ r_job_number then + @job.remote_id = $1 + end + + # check build result + if l.include? "Job is stopped by ERROR" or + l.include? "Error:" then + error = true + end + + # gather result files if not reverse build + if not is_rev_build and l =~ /Creating package file \.\.\. (.*)/ then + file_name = $1 + result_files.push file_name + end + end + if not result then + @log.error( "Communication failed! #{client.get_error_msg()}", Log::LV_USER) + end + if error then result=false end + + # close socket + client.terminate + + return result, result_files + end + + + # receive binary package of remote server + protected + def receive_file_from_remote(file_path, dock = "0") + # create client + client = BuildCommClient.create( @addr, @port, @log ) + if client.nil? then + @log.error( "Creating communication client failed!", Log::LV_USER) + return false + end + + # download file + result = true + file_name = file_path.split("/")[-1] + msg = "DOWNLOAD|#{dock}|#{file_name}" + if client.send( msg ) then + if not @ftp_addr.nil? then + transporter=FileTransferFTP.new(@log, @ftp_addr, @ftp_port, @ftp_username, @ftp_passwd) + else + transporter=FileTransferDirect.new(@log) + end + result=client.receive_file(file_path, transporter) + if not result then + @log.error( "File downloading failed...#{file_name}", Log::LV_USER) + end + end + + #close connections + client.terminate + + return result + end +end diff --git a/src/build_server/ReverseBuildChecker.rb b/src/build_server/ReverseBuildChecker.rb new file mode 100644 index 0000000..49dde68 --- /dev/null +++ b/src/build_server/ReverseBuildChecker.rb @@ -0,0 +1,217 @@ +=begin + + ReverseBuildChecker.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "log" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/pkg_server" +require "utils.rb" +require "client.rb" +require "JobLog.rb" +require "PackageManifest.rb" + +class ReverseBuildChecker + + # check + def ReverseBuildChecker.check( job, exit_on_error, override_os = nil ) + log = job.log + job_os = (override_os.nil?) ? job.os : override_os + + # start + log.info( "Checking reverse build dependency ...", Log::LV_USER) + + # get target packages that be checked + bin_pkg_name_list = [] + src_pkg_name_list = [] + case job.type + when "BUILD" + job.pkginfo.get_target_packages(job_os).each do |pkg| + bin_pkg_name_list.push pkg.package_name + end + when "REGISTER" + if job.pkg_type == "BINARY" then + bin_pkg_name_list.push job.pkg_name + else + src_pkg_name_list.push job.pkg_name + end + end + + # get reverse projects from build dependency + rev_pkgs = [] + bin_pkg_name_list.each do |pkg_name| + rev_pkgs += job.pkgsvr_client.get_reverse_build_dependent_packages(pkg_name, job_os) + end + src_pkg_name_list.each do |pkg_name| + rev_pkgs += job.pkgsvr_client.get_reverse_source_dependent_packages(pkg_name) + end + rev_pkgs.uniq! + rev_projects = job.server.prjmgr.get_projects_from_pkgs(rev_pkgs, job.get_distribution_name()) + + # create reverse build job + rev_build_jobs = [] + rev_projects.each do |p| + rev_prj = p[0] + rev_os = p[1] + rev_ver = p[2] + + # if not "GIT" project, ignore it + if rev_prj.type != "GIT" then next end + + # if job on resolve process, its unresolved project + #of pending ancestor must be excluded. + if job.type == "BUILD" and not job.pending_ancestor.nil? then + found = false + job.pending_ancestor.rev_fail_projects.each do |fp| + f_prj = fp[0] + f_os = fp[1] + + if rev_prj == f_prj and rev_os == f_os then + found = true + break + end + end + if found then next end + end + + # if this is sub job, all other sibling job must be excluded + if job.is_sub_job? then + job.get_parent_job().get_sub_jobs().each do |sub_job| + sub_prj = sub_job.get_project() + sub_os = sub_job.os + if rev_prj == sub_prj and rev_os == sub_os then + found = true + break + end + end + if found then next end + end + + # create job + new_job = rev_prj.create_new_job_from_version( rev_os, rev_ver ) + new_job.set_rev_build_check_job( job ) + + # set user id + new_job.user_id = job.user_id + + rev_build_jobs.push new_job + end + + # reverse build + if rev_build_jobs.count > 0 then + rev_prjs_msg = rev_build_jobs.map {|j| "#{j.get_project().name}(#{j.os})"}.join(", ") + log.info( " * Will check reverse-build for projects: #{rev_prjs_msg}", Log::LV_USER) + end + + # for all reverse job + rev_build_jobs.each do |rev_job| + # add to job manager + job.server.jobmgr.add_reverse_build_job(rev_job) + log.info( " * Added new job for reverse-build ... #{rev_job.get_project().name}(#{rev_job.os}) (#{rev_job.id})", Log::LV_USER) + end + + # wait for job finish + rev_build_finished = false + success_list = [] + failure_list = [] + cancel_other_jobs = false + while not rev_build_finished + rev_build_finished = true + rev_build_jobs.each do |rev_job| + rev_prj = rev_job.get_project() + rev_os = rev_job.os + + case rev_job.status + when "ERROR", "CANCELED" + # add fail list + if not is_project_included?(failure_list, rev_prj, rev_os) then + log.info( " * Reverse-build FAIL ... #{rev_prj.name}(#{rev_os}) (#{rev_job.id})", Log::LV_USER) + failure_list.push [ rev_prj, rev_os ] + write_log_url(log, rev_job) + end + + # if "exist on error" cancel all other jobs + if exit_on_error then + cancel_other_jobs = true + rev_build_jobs.each do |j| + if j.status != "ERROR" and j.status != "FINISHED" and + j.status != "CANCELED" and j.cancel_state == "NONE" then + + j.cancel_state = "INIT" + end + end + break + end + when "FINISHED" + # add success list + if not success_list.include? rev_job then + log.info( " * Reverse-build OK ... #{rev_prj.name}(#{rev_os}) (#{rev_job.id})", Log::LV_USER) + success_list.push rev_job + end + else + rev_build_finished = false + end + end + + sleep 1 + end + + # clean up all reverse build jobs + rev_build_jobs.each do |rev_job| + if rev_job.status == "ERROR" or rev_job.status == "CANCELED" then + rev_job.server.cleaner.clean_afterwards(rev_job.id) + else + rev_job.server.cleaner.clean(rev_job.id) + end + end + + return failure_list + end + + + private + def self.is_project_included?( prj_list, prj, os ) + prj_list.each do |p| + if p[0] == prj and p[1] == os then return true end + end + + return false + end + + + # write web url for log + private + def self.write_log_url(log, job) + url,remote_url = job.get_log_url() + if not url.empty? then + log.info( " ** Log1: #{url}", Log::LV_USER) + end + if not remote_url.empty? then + log.info( " ** Log2: #{remote_url}", Log::LV_USER) + end + end +end diff --git a/src/build_server/SocketJobRequestListener.rb b/src/build_server/SocketJobRequestListener.rb index 4cfa794..dc18da6 100644 --- a/src/build_server/SocketJobRequestListener.rb +++ b/src/build_server/SocketJobRequestListener.rb @@ -1,5 +1,5 @@ =begin - + SocketJobRequestListener.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -27,10 +27,9 @@ Contributors: =end $LOAD_PATH.unshift File.dirname(__FILE__) -require "GitBuildJob.rb" -require "LocalBuildJob.rb" require "JobLog.rb" require "BuildComm.rb" +require "BuildServerException.rb" class SocketJobRequestListener @@ -40,14 +39,23 @@ class SocketJobRequestListener @parent_server = parent @thread = nil @finish_loop = false + @comm_server = nil @log = @parent_server.log end # start listening def start() - @thread = Thread.new { - main() - } + @thread = Thread.new do + # make loop recover when unhandled exception occurred + while not @finish_loop + begin + main() + rescue => e + @log.error e.message + @log.error e.backtrace.inspect + end + end + end end @@ -56,36 +64,45 @@ class SocketJobRequestListener def stop_listening() @finish_loop = true end - + private - # thread main + # thread main def main() # server open begin - server = BuildCommServer.new(@parent_server.port, @log) + if not @parent_server.ftp_addr.nil? then + ftp_url = Utils.generate_ftp_url(@parent_server.ftp_addr, @parent_server.ftp_port, + @parent_server.ftp_username, @parent_server.ftp_passwd) + else + ftp_url = nil + end + cache_dir = "#{@parent_server.transport_path}/.cache" + @comm_server = BuildCommServer.create(@parent_server.port, @log, ftp_url, cache_dir) rescue @log.info "Server creation failed" + puts "Server creation failed" + @parent_server.stop return end # loop - @log.info "Entering Control Listening Loop ... " + @log.info "Entering Control Listening Loop ... " @finish_loop = false - server.wait_for_connection(@finish_loop) do |req| - handle_job_request( req ) - end + @comm_server.wait_for_connection(@finish_loop) do |req| + handle_job_request( req ) + end # quit - server.terminate + @comm_server.terminate end # wait for job requests def wait_for_job_requests req_list = [] - req_list.push @tcp_server.accept - + req_list.push @tcp_server.accept + return req_list end @@ -94,15 +111,18 @@ class SocketJobRequestListener def handle_job_request( req ) # read request - req_line = req.gets + req_line = req.gets if req_line.nil? then return end + # accept + BuildCommServer.send_begin(req) + # parse request cmd = "" - if req_line.split(",").count > 0 then - cmd = req_line.split(",")[0].strip + if req_line.split("|").count > 0 then + cmd = req_line.split("|")[0].strip end - + case cmd when "BUILD" handle_cmd_build( req_line, req ) @@ -110,142 +130,922 @@ class SocketJobRequestListener handle_cmd_resolve( req_line, req ) when "QUERY" handle_cmd_query( req_line, req ) + when "CANCEL" + handle_cmd_cancel( req_line, req ) + when "STOP" + handle_cmd_stop( req_line, req ) + when "UPGRADE" + handle_cmd_upgrade( req_line, req ) + when "FULLBUILD" + handle_cmd_fullbuild( req_line, req ) + when "REGISTER" + handle_cmd_register( req_line, req ) + when "DOWNLOAD" + Thread.new do + begin + handle_cmd_download( req_line, req ) + rescue => e + @log.error "Transfering file failed!" + @log.error e.message + @log.error e.backtrace.inspect + end + end + when "UPLOAD" + Thread.new do + begin + handle_cmd_upload( req_line, req ) + rescue => e + @log.error "Transfering file failed!" + @log.error e.message + @log.error e.backtrace.inspect + end + end else - @log.info "Received Unknown REQ: #{req_line}" + @log.info "Received Unknown REQ: #{req_line}" raise "Unknown request: #{req_line}" end end - # "BUILD" + # "BUILD" def handle_cmd_build( line, req ) - tok = line.split(",").map { |x| x.strip } - if tok.count < 4 then - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" + @log.info "Received REQ: #{line}" + + begin + handle_cmd_build_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) end - case tok[1] - # BUILD,GIT,repos,commit,os,url,async - when "GIT" - @log.info "Received BUILD GIT => #{tok[2]}" + @log.info "Handled REQ: #{line}" + end - # check asynchronous job - async = (not tok[6].nil? and tok[6]=="YES" ? true:false) - if async then - new_job = GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, nil, false) - else - new_job = GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, req, false) - end - BuildCommServer.send_begin(req) - # start job. If log url is supported, show it - if not @parent_server.job_log_url.empty? then - new_job.log.info( "Added new job \"#{new_job.id}\"! Check following URL", Log::LV_USER) - new_job.log.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) - else - new_job.log.info( "Added new job \"#{new_job.id}\"!", Log::LV_USER) + def handle_cmd_build_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then + raise BuildServerException.new("ERR001"), line + end + + # check type + if tok[1] != "GIT" then + raise BuildServerException.new("ERR001"), line + end + + # 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 + # Case1. BUILD|GIT|project_name|passwd|os_list|async|no_reverse|dist_name|user_email|verbose + # Case2. BUILD|GIT|git_repos | |os |async|no_reverse|dist_name|user_email|verbose|internal|rev_build|commit|pkgs|dock_num + + # parse + project_name_list = tok[2].split(",") + passwd_list = tok[3].split(",") + passwd = passwd_list[0] + os_list = tok[4].split(",") + async = tok[5].eql? "YES" + no_reverse = tok[6].eql? "YES" + dist_name = (not tok[7].nil? and not tok[7].empty?) ? tok[7].strip : "" + user_email = (not tok[8].nil? and not tok[8].empty?) ? tok[8].strip : "" + verbose = tok[9].eql? "YES" + is_internal = tok[10].eql? "YES" + rev_job = tok[11].eql? "YES" + git_commit = (not tok[12].nil? and not tok[12].empty?) ? tok[12] : nil + pkg_files = (not tok[13].nil? and not tok[13].empty?) ? tok[13].split(",") : [] + dock_num = (not tok[14].nil? and not tok[14].empty?) ? tok[14].strip : "0" + if (dist_name.nil? or dist_name.empty?) then + dist_name = @parent_server.distmgr.get_default_distribution_name() + end + + # check distribution + check_distribution(dist_name, req) + + # check supported os if not internal job + if not is_internal then + os_list = check_supported_os( os_list , req ) + end + + # check user email + user_id = @parent_server.check_user_id_from_email( user_email ) + if user_id == -1 then + raise BuildServerException.new("ERR004"), user_email + end + + + # multi build job + if project_name_list.count > 1 or os_list.count > 1 then + new_job_list = [] + i = 0 + project_name_list.each do |pname| + if not passwd_list[i].nil? then passwd = passwd_list[i] + else passwd = passwd_list[0] end + check_build_project(pname,passwd,dist_name,req) + if not check_project_user_id(pname,dist_name,user_id) then + raise BuildServerException.new("ERR005"), "#{user_email} -> #{pname}" + end + os_list.each do |os| + new_job = create_new_job( pname, os, dist_name ) + if new_job.nil? then + @log.warn "\"#{pname}\" does not support #{os}" + next + else + new_job.user_id = user_id + end + new_job_list.push new_job + @log.info "Received a request for building this project : #{pname}, #{os}" + end + i = i + 1 end - # if asynchronouse, quit connection - if async then - if not @parent_server.job_log_url.empty? then - req.puts ( "Info: Added new job \"#{new_job.id}\"! Check following URL") - req.puts ( "Info: * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log") + if new_job_list.count > 1 then + new_job = @parent_server.prjmgr.create_new_multi_build_job( new_job_list ) + if new_job.nil? then + raise BuildServerException.new("ERR006"),"Multi-Build job" else - req.puts( "Info: Added new job \"#{new_job.id}\"!") + new_job.user_id = user_id end + elsif new_job_list.count == 1 then + new_job = new_job_list[0] + else + raise BuildServerException.new("ERR006"),"No valid sub jobs in Multi-Build job" + end - BuildCommServer.send_end(req) - BuildCommServer.disconnect(req) - end + # transfered job + elsif is_internal then + git_repos = project_name_list[0] + os = os_list[0] - # add - @parent_server.add_job( new_job ) - - # BUILD,LOCAL,path,os,url - when "LOCAL" - @log.info "Received BUILD LOCAL => #{tok[2]}" - - BuildCommServer.send_begin(req) - @parent_server.add_job( - LocalBuildJob.new( tok[2], tok[3], tok[4], [], @parent_server, nil, req, false)) + new_job = create_new_internal_job(git_repos, os, git_commit, pkg_files, dock_num, dist_name ) + if new_job.nil? then + raise BuildServerException.new("ERR006"),"Transfered-Build job" + else + new_job.user_id = user_id + end + if rev_job then new_job.set_rev_build_check_job(nil) end + + # single job + elsif project_name_list.count == 1 and os_list.count == 1 then + pname = project_name_list[0] + os = os_list[0] + + check_build_project(pname,passwd,dist_name,req) + if not check_project_user_id(pname,dist_name,user_id) then + raise BuildServerException.new("ERR005"), "#{user_email} -> #{pname}" + end + new_job = create_new_job( pname, os, dist_name ) + if new_job.nil? then + raise BuildServerException.new("ERR006"), "\"#{pname}\" does not support #{os} in #{dist_name}" + else + new_job.user_id = user_id + end else - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" - end + raise BuildServerException.new("ERR006"), "Cannot find your project to build!" + end + + # check reverse build + if no_reverse then new_job.set_no_reverse end + + # create logger and set + if async then + new_job.create_logger( nil, verbose) + BuildCommServer.send(req,"Info: Added new job \"#{new_job.id}\" for #{new_job.os}!") + if not @parent_server.job_log_url.empty? then + BuildCommServer.send(req,"Info: * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log") + end + BuildCommServer.send(req,"Info: Above job(s) will be processed asynchronously!") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else + logger = new_job.create_logger( req, verbose) + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + end + + # add to job queue + if new_job.is_rev_build_check_job() then + @parent_server.jobmgr.add_reverse_build_job( new_job ) + else + @parent_server.jobmgr.add_job( new_job ) + end end - # "RESOLVE" - def handle_cmd_resolve( line ,req) - tok = line.split(",").map { |x| x.strip } - if tok.count < 4 then - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" + def check_build_project(prj_name, passwd, dist_name, req) + # check project + prj = check_project_exist(prj_name, dist_name, req) + + # check passwd + check_project_password(prj, passwd, req) + + # check project type + if prj.type == "BINARY" then + raise BuildServerException.new("ERR010"), prj.type + end + end + + + # "RESOLVE" + def handle_cmd_resolve( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_resolve_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_resolve_internal( line ,req) + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then + raise BuildServerException.new("ERR001"), line end case tok[1] - # RESOLVE,GIT,repos,commit,os,url + # RESOLVE|GIT|project_name|passwd|os|async|dist_name|user_email|verbose when "GIT" - @log.info "Received RESOLVE GIT => #{tok[2]}" - - BuildCommServer.send_begin(req) - @parent_server.add_job( - GitBuildJob.new( tok[2], tok[3], tok[4], tok[5], [], @parent_server, nil, req, true)) - # RESOLVE,LOCAL,path,os,url - when "LOCAL" - @log.info "Received RESOLVE LOCAL => #{tok[2]}" - - BuildCommServer.send_begin(req) - @parent_server.add_job( - LocalBuildJob.new( tok[2], tok[3], tok[4], [], @parent_server, nil, req, true)) + + # parse + project_name=tok[2] + passwd=tok[3] + os=tok[4] + async = tok[5].eql? "YES" + dist_name = tok[6] + user_email = (not tok[7].nil? and not tok[7].empty?) ? tok[7].strip : "" + verbose = tok[8].eql? "YES" + if (dist_name.nil? or dist_name.empty?) then + dist_name = @parent_server.distmgr.get_default_distribution_name() + end + + # check distribution + check_distribution(dist_name, req) + + # check project + prj = check_project_exist(project_name, dist_name, req) + + # check passwd + check_project_password(prj, passwd, req) + + # check os + os_list = check_supported_os( [os] , req ) + os = os_list[0] + + # check user email + user_id = @parent_server.check_user_id_from_email( user_email ) + if user_id == -1 then + raise BuildServerException.new("ERR004"), user_email + end + + # check user accessable + if not check_project_user_id(project_name,dist_name,user_id) then + raise BuildServerException.new("ERR005"), "#{user_email} -> #{project_name}" + end + + # create new job + new_job = create_new_job( project_name, os, dist_name ) + if new_job.nil? then + raise BuildServerException.new("ERR006"), "Resolve job #{project_name} #{os}" + end + @log.info "Received a request for resolving this project : #{project_name}, #{os}" + + new_job.user_id = user_id + + # resolve + new_job.set_resolve_flag() + + # create logger and set + if async then + new_job.create_logger( nil, verbose) + BuildCommServer.send(req,"Info: Added new job \"#{new_job.id}\" for #{new_job.os}!") + if not @parent_server.job_log_url.empty? then + BuildCommServer.send(req,"Info: * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log") + end + BuildCommServer.send(req,"Info: Above job(s) will be processed asynchronously!") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else + logger = new_job.create_logger( req, verbose) + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + end + + @parent_server.jobmgr.add_job( new_job ) else - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" - end + raise BuildServerException.new("ERR001"), line + end end # "QUERY" def handle_cmd_query( line, req ) - tok = line.split(",").map { |x| x.strip } - if tok.count < 2 then - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" + begin + handle_cmd_query_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) end - + end + + + def handle_cmd_query_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + raise BuildServerException.new("ERR001"), line + end + case tok[1] - # QUERY,JOB + + # QUERY, FTP + when "FTP" + if not @parent_server.ftp_addr.nil? then + BuildCommServer.send(req,"#{@parent_server.ftp_addr},#{@parent_server.ftp_username},#{@parent_server.ftp_passwd}") + else + BuildCommServer.send(req,"NONE,NONE,NONE") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + # QUERY,JOB when "JOB" #puts "Received QUERY JOB" - BuildCommServer.send_begin(req) - for job in @parent_server.working_jobs - BuildCommServer.send(req,"WORKING,#{job.id},#{job.pkginfo.packages[0].source}") + # gather all jobs to show + job_list = @parent_server.jobmgr.jobs + @parent_server.jobmgr.internal_jobs + @parent_server.jobmgr.reverse_build_jobs + + # send the status + job_list.each do |job| + status = job.status + if status == "REMOTE_WORKING" then status = "REMOTE" end + if job.cancel_state != "NONE" then status = "CANCEL" end + + case job.type + when "BUILD" + if status == "PENDING" then + if job.pending_ancestor.nil? then + ids = "/" + else + ids = job.pending_ancestor.id + end + BuildCommServer.send(req,"#{status}:#{ids},#{job.id},#{job.get_project().name},#{job.os} #{job.progress},#{job.get_distribution_name}") + else + BuildCommServer.send(req,"#{status},#{job.id},#{job.get_project().name},#{job.os} #{job.progress},#{job.get_distribution_name}") + end + when "REGISTER" + if job.pkg_type == "BINARY" and not job.get_project().nil? then + BuildCommServer.send(req,"#{status},#{job.id},#{job.get_project().name},#{job.os} #{job.progress},#{job.get_distribution_name}") + else + BuildCommServer.send(req,"#{status},#{job.id},#{job.pkg_name},#{job.get_distribution_name}") + end + when "MULTIBUILD" + BuildCommServer.send(req,"#{status},#{job.id},MULTI-BUILD : #{job.get_sub_jobs().map{|x| x.id}.join(" ")},#{job.get_distribution_name}") + end end - for job in @parent_server.waiting_jobs - BuildCommServer.send(req,"WAITING,#{job.id},#{job.pkginfo.packages[0].source}") + + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + # QUERY,SYSTEM + when "SYSTEM" + #puts "Received QUERY SYSTEM" + + BuildCommServer.send(req,"#{@parent_server.host_os},#{@parent_server.jobmgr.max_working_jobs}") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + when "PROJECT" + # print GIT projects + sorted_list = @parent_server.prjmgr.get_all_projects_summary().sort { |x,y| x[:name] <=> y[:name] } + sorted_list.select{|x| x[:ptype] == "GIT"}.each do |prj| + BuildCommServer.send(req,"G,#{prj[:name]},#{prj[:dist_name]}") + end + # print BINARY projects + sorted_list.select{|x| x[:ptype] == "BINARY"}.each do |prj| + BuildCommServer.send(req,"B,#{prj[:name]},#{prj[:dist_name]}") end - for job in @parent_server.remote_jobs - BuildCommServer.send(req,"REMOTE ,#{job.id},#{job.pkginfo.packages[0].source}") + # print REMOTE project + sorted_list.select{|x| x[:ptype] == "REMOTE"}.each do |prj| + BuildCommServer.send(req,"R,#{prj[:name]},#{prj[:dist_name]}") end BuildCommServer.send_end(req) BuildCommServer.disconnect(req) - # QUERY,SYSTEM - when "SYSTEM" - #puts "Received QUERY SYSTEM" + when "OS" + # print GIT projects + @parent_server.supported_os_list.each do |os_name| + BuildCommServer.send(req,"#{os_name}") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + when "FRIEND" + # print GIT projects + @parent_server.remote_servers.each do |server| + BuildCommServer.send(req,"#{server.status},#{server.host_os},#{server.waiting_job_count},#{server.working_job_count},#{server.max_working_jobs},#{server.get_file_transfer_cnt}") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + + else + raise BuildServerException.new("ERR001"), line + end + end + + + # "CANCEL" + def handle_cmd_cancel( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_cancel_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_cancel_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + raise BuildServerException.new("ERR001"), line + end + cancel_job = nil + + # check user email + user_id = @parent_server.check_user_id_from_email( tok[3] ) + if user_id == -1 then + raise BuildServerException.new("ERR004"), tok[3] + end + + #CANCEL, JOB + (@parent_server.jobmgr.jobs + @parent_server.jobmgr.internal_jobs + @parent_server.jobmgr.reverse_build_jobs).each do |j| + if "#{j.id}" == "#{tok[1]}" then + cancel_job = j + break + end + end + + if cancel_job.nil? then + raise BuildServerException.new("ERR014"), "Job #{tok[1]} not found." + else + if cancel_job.cancel_state == "NONE" then + # check passwd + if not @parent_server.jobmgr.is_user_accessable(cancel_job,user_id) then + raise BuildServerException.new("ERR014"), "Access denied #{tok[3]}" + end + if cancel_job.type == "MULTIBUILD" then + cancel_job.get_sub_jobs().select{|x| x.cancel_state == "NONE" }.each do |sub| + check_project_password( sub.get_project, tok[2], req) + end + + BuildCommServer.send(req, "\"#{cancel_job.id}, #{cancel_job.get_sub_jobs().map{|x| x.id}.join(", ")}\" will be canceled") + cancel_job.cancel_state = "INIT" + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else + prj = cancel_job.get_project() + if not prj.nil? then + check_project_password( prj, tok[2], req) + + BuildCommServer.send(req, "\"#{cancel_job.id}\" will be canceled") + cancel_job.cancel_state = "INIT" + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + else + raise BuildServerException.new("ERR014"), "No project infomation" + end + end + else + raise BuildServerException.new("ERR014"), "Job already canceled." + end + end + end + + + # "STOP" + def handle_cmd_stop( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_stop_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_stop_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + raise BuildServerException.new("ERR001"), line + end + + if tok[1] != @parent_server.password then + raise BuildServerException.new("ERR015"), "" + else + BuildCommServer.send(req,"Server will be down!") + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + if tok[1] == @parent_server.password then + @parent_server.finish = true + end + end + - BuildCommServer.send_begin(req) - BuildCommServer.send(req,"#{@parent_server.host_os},#{@parent_server.max_working_jobs}") + # "UPGRADE" + def handle_cmd_upgrade( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_upgrade_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_upgrade_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + raise BuildServerException.new("ERR001"), line + end + + if tok[1] != @parent_server.password then + raise BuildServerException.new("ERR015"), "" + else + BuildCommServer.send(req,"Server will be upgraded!") BuildCommServer.send_end(req) BuildCommServer.disconnect(req) + end + if tok[1] == @parent_server.password then + @parent_server.finish = true + @parent_server.upgrade = true + end + end + + + # "FULLBUILD" + def handle_cmd_fullbuild( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_fullbuild_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_fullbuild_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + raise BuildServerException.new("ERR001"), line + end + + server_passwd = tok[1] + dist_name = tok[2] + if (dist_name.nil? or dist_name.empty?) then + dist_name = @parent_server.distmgr.get_default_distribution_name() + end + + # check distribution + check_distribution(dist_name, req, true) + + # check server password + if server_passwd != @parent_server.password then + raise BuildServerException.new("ERR015"), "" + end + + # create full build job + new_job = @parent_server.prjmgr.create_new_full_build_job(dist_name) + + # set logger + new_job.create_logger( req ) + + # add to job + @parent_server.jobmgr.add_job( new_job ) + end + + + # "REGISTER" + def handle_cmd_register( line, req ) + @log.info "Received REQ: #{line}" + + begin + handle_cmd_register_internal( line, req ) + rescue BuildServerException => e + @log.error(e.message) + BuildCommServer.send(req, e.err_message()) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + @log.info "Handled REQ: #{line}" + end + + + def handle_cmd_register_internal( line, req ) + tok = line.split("|").map { |x| x.strip } + if tok.count < 4 then + raise BuildServerException.new("ERR001"), line + end + + type = tok[1] + + case type + # REGISTER|BINARY-LOCAL|local_path|passwd|dist_name + # REGISTER|SOURCE-LOCAL|local_path|passwd|dist_name + when "BINARY-LOCAL", "SOURCE-LOCAL" + file_path = tok[2] + dist_name = tok[4] + if (dist_name.nil? or dist_name.empty?) then + dist_name = @parent_server.distmgr.get_default_distribution_name() + end + + # check distribution + check_distribution(dist_name, req) + + new_job = @parent_server.jobmgr.create_new_register_job( file_path, dist_name ) + new_job.create_logger( req ) + + # add + @parent_server.jobmgr.add_job( new_job ) + + # REGISTER|BINARY|filename|passwd|dock|dist_name|user_email|no_reverse + when "BINARY" + # parse + filename = tok[2] + passwd = tok[3] + dock = (tok[4].nil? or tok[4].empty?) ? "0" : tok[4].strip + dist_name = tok[5] + user_email = (tok[7].nil? or tok[7].empty?) ? "" : tok[6].strip + no_reverse = tok[7].eql? "YES" + + if (dist_name.nil? or dist_name.empty?) then + dist_name = @parent_server.distmgr.get_default_distribution_name() + end + + # check distribution + check_distribution(dist_name, req) + + # check project + prj = check_project_for_package_file_name(filename, dist_name, req) + + # check user email + user_id = @parent_server.check_user_id_from_email( user_email ) + if user_id == -1 then + raise BuildServerException.new("ERR004"), user_email + end + + if not check_project_pkg_name_user_id(filename, dist_name, user_id) then + raise BuildServerException.new("ERR005"), "#{user_email} -> #{prj.name}" + end + + # check passwd + check_project_password(prj, passwd, req) + + # create new job + @log.info "Received a request for uploading binaries : #{filename}" + new_job = create_new_upload_job( prj.name, filename, dock, dist_name, req ) + if new_job.nil? then + raise BuildServerException.new("ERR006"), "Register-job #{filename}, #{prj.name}, #{dist_name}" + end + + new_job.user_id = user_id + + # check reverse build + if no_reverse then new_job.set_no_reverse end + + # create logger and set + logger = new_job.create_logger(req) + + # notify that job has been received + logger.info( "Added new job \"#{new_job.id}\" for #{new_job.os}!", Log::LV_USER) + if not @parent_server.job_log_url.empty? then + logger.info( " * Log URL : #{@parent_server.job_log_url}/#{new_job.id}/log", Log::LV_USER) + end + + # add + @parent_server.jobmgr.add_job( new_job ) else - @log.info "Received Wrong REQ: #{line}" - raise "Invalid request format is used: #{line}" + raise BuildServerException.new("ERR001"), line + end + + end + + + # "UPLOAD" + def handle_cmd_upload( line, req ) + @log.info "Received File transfer REQ : #{line}" + + tok = line.split("|").map { |x| x.strip } + dock_num = (tok[1].nil? or tok[1].empty?) ? "0" : tok[1].strip + + incoming_dir = "#{@parent_server.transport_path}/#{dock_num}" + if not File.exist? incoming_dir then FileUtils.mkdir_p incoming_dir end + @comm_server.receive_file( req, incoming_dir ) + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + + # "DOWNLOAD" + # format = DOWNLOAD|dock_num|file_name + def handle_cmd_download( line, req ) + @log.info "Received File transfer REQ : #{line}" + tok = line.split("|").map { |x| x.strip } + dock_num = (tok[1].nil? or tok[1].empty?) ? "0" : tok[1].strip + file_name = tok[2] + + @log.info "Received a request for download file : #{file_name}" + outgoing_dir = "#{@parent_server.transport_path}/#{dock_num}" + @log.info "Sending requested file...: #{file_name}" + @comm_server.send_file(req, "#{outgoing_dir}/#{file_name}") + # remove file if "dock" defined + if dock_num != "0" and File.exist? "#{outgoing_dir}/#{file_name}" then + @log.info "Removing requested file...: #{file_name}" + FileUtils.rm_rf "#{outgoing_dir}/#{file_name}" + if Utils.directory_emtpy?(outgoing_dir) then + FileUtils.rm_rf "#{outgoing_dir}" + end end + + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end + + + private + def check_project_user_id(project_name, dist_name, user_id) + return @parent_server.prjmgr.get_project_accessable(project_name, dist_name, user_id) + end + + private + def check_project_exist(project_name, dist_name, req) + prj = @parent_server.prjmgr.get_project(project_name, dist_name) + if prj.nil? then + raise BuildServerException.new("ERR009"), "#{project_name} on #{dist_name}" + end + + return prj + end + + private + def check_project_pkg_name_user_id(file_name, dist_name, user_id) + # get package name + new_name = file_name.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + pkg_name = new_name.split(",")[0] + return @parent_server.prjmgr.get_project_pkg_name_accessable(pkg_name, dist_name, user_id) end + private + def check_project_for_package_file_name(filename, dist_name, req) + # get package name + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + pkg_name = new_name.split(",")[0] + + prj = @parent_server.prjmgr.get_project_from_package_name(pkg_name, dist_name) + if prj.nil? then + raise BuildServerException.new("ERR013"), "#{pkg_name} #{dist_name}" + end + + return prj + end + + + private + def check_project_password(prj, passwd, req) + + if prj.is_passwd_set? then + if passwd.nil? or passwd.empty? then + raise BuildServerException.new("ERR011"), "Use -w option to input your project password" + end + + if not prj.passwd_match?(passwd) then + raise BuildServerException.new("ERR012"), "" + end + end + end + + + private + def check_distribution(dist_name, req, only_exist = false) + dist = @parent_server.distmgr.get_distribution(dist_name) + if dist.nil? then + raise BuildServerException.new("ERR002"), dist_name + elsif dist.status != "OPEN" and not only_exist then + raise BuildServerException.new("ERR008"), dist_name + end + end + + + private + def check_supported_os(os_list, req) + + # check if supported os list contain at least one OS + if @parent_server.supported_os_list.empty? then + raise BuildServerException.new("ERR007"), "" + end + + result = [] + os_list.each do |os| + if os == "all" or os == "*" then + result = result + @parent_server.supported_os_list + + elsif os == "default" then + os = @parent_server.supported_os_list[0] + result.push os + @log.info "The default OS \"#{os}\" is used as target OS" + + elsif os.include? "*" then + reg_os = os.gsub("*","[a-zA-Z0-9.]*") + @parent_server.supported_os_list.each do |svr_os| + matches = svr_os.match("#{reg_os}") + if not matches.nil? and matches.size == 1 and + matches[0] == svr_os then + result.push svr_os + end + end + elsif @parent_server.supported_os_list.include?(os) then + result.push os + else + msgs = "#{os}\n\tSupported OS list.\n" + @parent_server.supported_os_list.each do |os_name| + msgs += "\t * #{os_name}\n" + end + raise BuildServerException.new("ERR003"),msgs + end + end + + result.uniq! + if result.empty? then + raise BuildServerException.new("ERR003"), "There is no OS name matched." + end + + return result + end + + + private + def create_new_job( project_name, os, dist_name ) + return @parent_server.prjmgr.create_new_job(project_name, os, dist_name) + end + + + private + def create_new_upload_job( project_name, filename, dock, dist_name, req) + + return @parent_server.prjmgr.get_project(project_name, dist_name).create_new_job(filename, dock) + end + + + private + def create_new_internal_job( git_repos, os, git_commit, pkg_files, dock_num, dist_name ) + prj = @parent_server.prjmgr.get_git_project( git_repos, dist_name ) + if prj.nil? then + prj = @parent_server.prjmgr.create_unnamed_git_project( git_repos, dist_name ) + end + new_job = prj.create_new_job(os) + new_job.set_internal_job( dock_num ) + new_job.git_commit = git_commit + incoming_dir = "#{@parent_server.transport_path}/#{dock_num}" + pkg_files.each do |file| + new_job.add_external_package( file ) + end + + return new_job + end end diff --git a/src/builder/Builder.rb b/src/builder/Builder.rb index a9e672c..386cb94 100644 --- a/src/builder/Builder.rb +++ b/src/builder/Builder.rb @@ -1,5 +1,5 @@ =begin - + Builder.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -36,22 +36,29 @@ require "log" class Builder private_class_method :new - attr_accessor :id, :pkgserver_url, :log + attr_accessor :id, :pkgserver_url, :log, :buildroot_dir, :cache_dir CONFIG_ROOT = Utils::HOME + "/.build_tools/builder" @@instance_map = {} # initialize - def initialize (id, pkgserver_url, log_path) + def initialize (id, pkgserver_url, log_path, buildroot_dir, cache_dir) @id = id @pkgserver_url = pkgserver_url @host_os = Utils::HOST_OS - @log = Log.new(log_path) + @buildroot_dir = buildroot_dir + @cache_dir = cache_dir + @job = nil + if not log_path.nil? then + @log = Log.new(log_path) + else + @log = DummyLog.new + end end - + # create - def self.create (id, pkgserver_url, log_path) + def self.create (id, pkgserver_url, log_path, buildroot_dir, cache_dir) # check builder config root check_builder_config_root @@ -61,8 +68,24 @@ class Builder FileUtils.rm_rf "#{CONFIG_ROOT}/#{id}" end + # create buildroot if not set + if buildroot_dir.nil? then + buildroot_dir = "#{CONFIG_ROOT}/#{id}/buildroot" + if not File.exist? buildroot_dir then + FileUtils.mkdir_p buildroot_dir + end + end + + # create cachedir if not set + if cache_dir.nil? then + cache_dir = "#{CONFIG_ROOT}/#{id}/build_cache" + if not File.exist? cache_dir then + FileUtils.mkdir_p cache_dir + end + end + # create new instance and return it - @@instance_map[id] = new( id, pkgserver_url, log_path ) + @@instance_map[id] = new( id, pkgserver_url, log_path, buildroot_dir, cache_dir ) # write config write_builder_config( @@instance_map[id] ) @@ -83,16 +106,28 @@ class Builder end + def self.exist?( id ) + # check builder config root + check_builder_config_root + + # check id + if File.exist? "#{CONFIG_ROOT}/#{id}" then + return true + else + return false + end + end + # get def self.get( id ) # check instance first if not @@instance_map[id] == nil - return @@instance_map[id] + return @@instance_map[id] end - # check builder config - if not File.exist? "#{CONFIG_ROOT}/#{id}/builder.cfg" + # check builder config + if not File.exist? "#{CONFIG_ROOT}/#{id}/builder.cfg" raise RuntimeError, "The builder \"#{id}\" does not exist." end @@ -106,68 +141,109 @@ class Builder # clean def clean( src_path ) - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" + return clean_project_directory( src_path, nil ) + end - # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") - # make clean - for pkg in pkginfo.packages - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.linux" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.windows" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.darwin" - end - env_def = - "SRCDIR=\"#{src_path}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} clean" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on clean script", Log::LV_USER ) - return false - end + # build_job + def build_job( job, clean, local_pkgs, is_local_build ) + # set job + @job = job + old_log = @log + @log = job.log - return true + # build + ret = build(job.source_path, job.os, clean, local_pkgs, is_local_build) + + # reset job + @job = nil + @log = old_log + + # return + return ret end # build - def build( src_path, os, clean, reverse_dep_check, pending_pkg_dir_list, ignore_rev_dep_build_list ) + def build( src_path, os, clean, local_pkgs, is_local_build ) # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + if not File.exist? "#{src_path}/package/pkginfo.manifest" then + @log.error( "The \"package/pkginfo.manifest\" file does not exist!", Log::LV_USER) + return false + end - # check there are packages which can be built - if not pkginfo.package_exist?(os, Utils::HOST_OS ) then - @log.error( "There are no packages which can be built on this host OS: #{Utils::HOST_OS}") - @log.error( " * Check \"Build-host-os\" in pkginfo.manifest" ) + # read pkginfo + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) return false end - # set build root - if clean then - build_root_dir = "#{CONFIG_ROOT}/#{@id}/temp_root" - else - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" + # set default build os + build_host_os = @host_os + + # check there are packages which can be built + if not pkginfo.package_exist?(os, build_host_os ) then + if is_local_build and File.exist? "#{src_path}/package/pkginfo.manifest.local" then + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest.local") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + if not pkginfo.package_exist?(os, build_host_os ) then + + @log.error( "This project does not support a build on this host OS: #{build_host_os}") + @log.error( " * Check \"Build-host-os\" in pkginfo.manifest or pkginfo.manifest.local" ) + + return false + end + else + @log.error( "This project does not support a build on this host OS: #{build_host_os}") + @log.error( " * Check \"Build-host-os\" in pkginfo.manifest" ) + + return false + end + end + + #set up change log + change_log = {} + begin + change_log = Parser.read_changelog "#{src_path}/package/changelog" if File.exist? "#{src_path}/package/changelog" + rescue => e + @log.error( e.message, Log::LV_USER) + return false end - FileUtils.mkdir_p build_root_dir + if not change_log.empty? and pkginfo.packages[0].change_log.empty? then + pkginfo.packages.each {|pkg| pkg.change_log = change_log} + end - local_pkg_list = [] - pending_pkg_dir_list.each do |dir| - local_pkg_list += Dir.entries(dir).select{|e| e =~ /\.zip$/}.map{|p| dir + "/" + p} - end + # set build root + build_root_dir = @buildroot_dir + if not File.exist? build_root_dir then + FileUtils.mkdir_p build_root_dir + end # create client @log.info( "Downloding client is initializing...", Log::LV_USER) - cl = Client.new(@pkgserver_url, build_root_dir, @log) - if clean then + cl = Client.new(@pkgserver_url, build_root_dir, @log) + if clean then cl.clean(true) end - cl.update + + # get local repository path list + repos_paths = [] + local_pkgs.each do |path| + repos_paths.push File.dirname(path) + end + repos_paths.uniq! # install build dependencies - package_overwrite_list = [] @log.info( "Installing dependent packages...", Log::LV_USER) - pkginfo.get_build_dependencies( os, @host_os ).each do |dep| + pkginfo.get_build_dependencies( os ).each do |dep| if dep.target_os_list.count != 0 then dep_target_os = dep.target_os_list[0] else @@ -175,75 +251,54 @@ class Builder end @log.info( " * #{dep.package_name}", Log::LV_USER) - # get local dependent package - pkgexp = Regexp.new("\/#{dep.package_name}_.*_#{dep_target_os}\.zip$") - package_overwrite_list += local_pkg_list.select{|l| l =~ pkgexp} + # get local dependent package + pkgexp = Regexp.new("\/#{dep.package_name}_.*_#{dep_target_os}\.zip$") + local_dep_pkgs = local_pkgs.select{|l| l =~ pkgexp} - if not cl.install(dep.package_name, dep_target_os, true, false) then - @log.error( "Installing \"#{dep.package_name}\" failed!", Log::LV_USER) - return false + # install package from remote package server + if local_dep_pkgs.empty? then + if not cl.install(dep.package_name, dep_target_os, true, false) then + @log.error( "Installing \"#{dep.package_name}\" failed!", Log::LV_USER) + return false + end + else + local_dep_pkgs.each do |l| + @log.info( "Installing local package...#{l}", Log::LV_USER) + if not File.exist? l then + @log.error( "File not found!: #{l}", Log::LV_USER ) + end + cl.install_local_pkg(l,true,false, repos_paths) + end end end - # overwrite local dependent packages - package_overwrite_list.each do |l| - cl.install_local_pkg(l,false) - end - @log.info( "Downloading dependent source packages...", Log::LV_USER) - pkginfo.get_source_dependencies(os,@host_os).each do |dep| - @log.info( " * #{dep.package_name}", Log::LV_USER) - - if cl.download_dep_source(dep.package_name).nil? then - @log.error( "Downloading \"#{dep.package_name}\" failed!", Log::LV_USER) - return false - end - end - - # make clean - @log.info( "Make clean...", Log::LV_USER) - for pkg in pkginfo.packages - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.linux" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.windows" - FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.darwin" - end - - # convert path if windows - if Utils::HOST_OS == "windows" then - build_root_dir = Utils.get_unix_path( build_root_dir ) + src_archive_list = [] + pkginfo.get_source_dependencies(os,build_host_os).each do |dep| + src_archive_list.push dep.package_name end - env_def = - "BUILD_TARGET_OS=#{os} \ - SRCDIR=\"#{src_path}\" \ - ROOTDIR=\"#{build_root_dir}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} clean" - - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on clean script", Log::LV_USER) - return false + src_archive_list.uniq! + src_archive_list.each do |archive_name| + @log.info( " * #{archive_name}", Log::LV_USER) + if cl.download_dep_source(archive_name).nil? then + @log.error( "Downloading \"#{archive_name}\" failed!", Log::LV_USER) + return false + end end - # make source package - @log.info( "Make source package...", Log::LV_USER) - build_command = "cd \"#{src_path}\";tar czf #{pkginfo.packages[0].source}_#{pkginfo.packages[0].version}.tar.gz --exclude=.git *" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on tar script", Log::LV_USER) + # make clean + @log.info( "Make clean...", Log::LV_USER) + if not clean_project_directory( src_path, os ) then return false end - # execute build script @log.info( "Make build...", Log::LV_USER) - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} build" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on build script", Log::LV_USER) + if not execute_build_command("build", src_path, build_root_dir, os, pkginfo.get_version) then return false end - # execute install script @log.info( "Make install...", Log::LV_USER) - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} install" - if not Utils.execute_shell_with_log( build_command, @log ) - @log.error( "Failed on build script", Log::LV_USER) + if not execute_build_command("install", src_path, build_root_dir, os, pkginfo.get_version) then return false end @@ -256,37 +311,12 @@ class Builder # zip @log.info( "Zipping...", Log::LV_USER) - make_zip(pkginfo,os,src_path) - - # check reverse dependecy if needed - if reverse_dep_check then - if not check_reverse_build_dependency_fail_list(src_path, os, cl, true, ignore_rev_dep_build_list).empty? then - return false - end + if not make_zip(pkginfo,os,src_path) then + @log.error( "Creating packages failed!", Log::LV_USER) + return false end - return true - end - def build_resolve(src_path, os, pending_pkg_dir_list, ignore_rev_dep_build_list) - # clean build - if not build(src_path, os, true, false, pending_pkg_dir_list, ignore_rev_dep_build_list) then - return nil - end - # init client - build_root_dir = "#{CONFIG_ROOT}/#{@id}/temp_root" - cl = Client.new(@pkgserver_url, build_root_dir, @log) - # rev build - return check_reverse_build_dependency_fail_list(src_path, os, cl, false, ignore_rev_dep_build_list ) - end - - - # reset - def reset() - build_root_dir = "#{CONFIG_ROOT}/#{@id}/buildroot" - temp_dir = cl.location - cl.location = build_root_dir - cl.clean(true) - cl.location = temp_dir + return true end @@ -307,15 +337,17 @@ class Builder def self.write_builder_config( obj ) # create config folder builder_dir = "#{CONFIG_ROOT}/#{obj.id}" - FileUtils.mkdir_p( "#{builder_dir}" ) + if not File.exist? builder_dir then + FileUtils.mkdir_p( "#{builder_dir}" ) + end # write configuration File.open( "#{builder_dir}/builder.cfg", "w" ) do |f| f.puts "ID=#{obj.id}" f.puts "PSERVER_URL=#{obj.pkgserver_url}" f.puts "LOG-PATH=#{obj.log.path}" + f.puts "CACHE-DIR=#{obj.cache_dir}" end - puts "#{builder_dir}/builder.cfg" end @@ -325,166 +357,169 @@ class Builder # read configuration builder_dir = "#{CONFIG_ROOT}/#{id}" - log_path = nil + log_path = nil + cache_dir = "#{CONFIG_ROOT}/#{id}/build_cache" + buildroot_dir = "#{CONFIG_ROOT}/#{id}/buildroot" File.open( "#{builder_dir}/builder.cfg", "r" ) do |f| f.each_line do |l| if l.start_with?("PSERVER_URL=") - pkgserver_url = l.split("=")[1].strip - elsif l.start_with?("LOG-PATH=") - log_path = l.split("=")[1].strip - log_path = nil if log_path == "STDOUT" + pkgserver_url = l.split("=")[1].strip + elsif l.start_with?("LOG-PATH=") + log_path = l.split("=")[1].strip + log_path = nil if log_path == "STDOUT" + elsif l.start_with?("CACHE-DIR=") + cache_dir = l.split("=")[1].strip + elsif l.start_with?("BUILDROOT-DIR=") + buildroot_dir = l.split("=")[1].strip else - next - end + next + end end end - if log_path.empty? then log_path = nil end - # create object & return it - return new( id, pkgserver_url, log_path ) - end + if log_path.empty? then log_path = nil end - - # check reverse build dependency - def check_reverse_build_dependency_fail_list( parent_path, os, pkg_cl, immediately, ignore_rev_dep_build_list ) - @log.info( "Checking reverse build dependency ...", Log::LV_USER) - - reverse_fail_list = [] - - # install newly packages - for path in Dir.glob("*_*_#{os}.zip") - # install - pkg_cl.install_local_pkg( path, false ) - end - - # get reverse-dependent source-codes - pkginfo = PackageManifest.new("#{parent_path}/package/pkginfo.manifest") - pkg_list = [] - for pkg in pkginfo.packages - pkg_list = pkg_list + pkg_cl.get_reverse_build_dependent_packages(pkg.package_name, os) - @log.info( "Extract reverse build dependency #{pkg.package_name} ...", Log::LV_USER) - end - pkg_list -= ignore_rev_dep_build_list - pkg_list.uniq! - - # download sources - src_path_hash = {} - pkg_list.each do |pkg| - # download - src_path = pkg_cl.download_source(pkg, os) - @log.info( "Downloaded #{pkg} source package to #{src_path}", Log::LV_USER) - - if src_path_hash[src_path].nil? then - src_path_hash[src_path] = [pkg] - else - src_path_hash[src_path] += [pkg] - end - end - src_path_list = src_path_hash.keys - - # add jobs for building reverse-dependent - src_path_list.each do |path| - # extract source package to test path - @log.info( " * Extracting source ... #{path}", Log::LV_USER) - test_src_path = "#{parent_path}/tmp_build" - FileUtils.mkdir_p test_src_path - Utils.execute_shell("cd \"#{test_src_path}\";tar xf #{path}") - - # build - @log.info( " * Building source ... ", Log::LV_USER) - result = build_test_with_pkg_client( pkg_cl, test_src_path, os, parent_path ) - FileUtils.rm_rf test_src_path - if not result then - reverse_fail_list += src_path_hash[path] - @log.error( "Build \"#{src_path_hash[path].join(", ")}\" test failed", Log::LV_USER) - if immediately then - return reverse_fail_list - end - end - end - - return reverse_fail_list.uniq + # create object & return it + return new( id, pkgserver_url, log_path, buildroot_dir, cache_dir ) end - # build test - def build_test_with_pkg_client( pkg_cl, src_path, os, parent_path) + # execute build command + def execute_build_command( target, src_path, build_root_dir, os, version ) - local_pkg_list = [] - local_pkg_list += Dir.entries(parent_path).select{|e| e =~ /\.zip$/}.map{|p| parent_path + "/" + p} + # get category + os_category = Utils.get_os_category( os ) - # create pkginfo - pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + # convert directory format when windows + if Utils.is_windows_like_os( @host_os ) then + src_path2 = Utils.get_unix_path( src_path ) + else + src_path2 = src_path + end - # install build dependencies - package_overwrite_list = [] - pkginfo.get_build_dependencies(os,@host_os).each do |dep| - if dep.target_os_list.count != 0 then - dep_target_os = dep.target_os_list[0] - else - dep_target_os = os + env_def = + "BUILD_TARGET_OS=#{os} \ +TARGET_OS=#{os} \ +TARGET_OS_CATEGORY=#{os_category} \ +SRCDIR=\"#{src_path2}\" \ +ROOTDIR=\"#{build_root_dir}\" \ +VERSION=\"#{version}\" " + + # check script file + script_file = "#{src_path}/package/build.#{@host_os}" + if not File.exist? script_file then + if Utils.is_linux_like_os( @host_os ) then + script_file = "#{src_path}/package/build.linux" + elsif Utils.is_windows_like_os( @host_os ) then + script_file = "#{src_path}/package/build.windows" + elsif Utils.is_macos_like_os( @host_os ) then + script_file = "#{src_path}/package/build.macos" + end + # check old script file + if not File.exist? script_file then + @log.error( "The script file not found!: \"package/build.#{@host_os}\"", Log::LV_USER) + return false end + end - # get local dependent package - pkgexp = Regexp.new("\/#{dep.package_name}_.*_#{dep_target_os}\.zip$") - package_overwrite_list += local_pkg_list.select{|l| l =~ pkgexp} + # read build script + # this will ignore last lines without block + contents = [] + File.open( script_file, "r" ) do |f| + lines = [] + f.each_line do |l| + lines.push l + if l.start_with? "}" then + contents = contents + lines + lines = [] + end + end + end - pkg_cl.install(dep.package_name, dep_target_os, true, false) - end + # generate shell script + File.open( "#{src_path}/.build.sh", "w" ) do |f| + f.puts "#!/bin/sh -xe" + contents.each do |l| + f.puts l + end - # overwrite local dependent packages - package_overwrite_list.each do |l| - @log.info( "Package overwrite ..#{l}", Log::LV_USER) - pkg_cl.install_local_pkg(l,false) - end + case target + when "clean" + f.puts " " + when "build" + f.puts " " + when "build_cache" + f.puts "CACHEDIR=${PKG_CACHE_DIR}/$(cache_key)" + when "save_cache" + f.puts "rm -rf ${PKG_CACHE_DIR}/*" + f.puts "CACHEDIR=${PKG_CACHE_DIR}/$(cache_key)" + f.puts "mkdir -p ${CACHEDIR}" + when "install" + f.puts " " + else + @log.warn( "Wrong build-target is used: \"#{target}\"", Log::LV_USER) + return false + end + f.puts "#{target}" + f.puts "echo \"success\"" + end + Utils.execute_shell( "chmod +x #{src_path}/.build.sh" ) + build_command = "cd \"#{src_path}\";" + env_def + "./.build.sh" - # source download - pkginfo.get_source_dependencies(os,@host_os).each do |dep| - pkg_cl.download_dep_source(dep.package_name) + # execute script + status = nil + if not @job.nil? then + pid, status = @job.execute_command( build_command ) + else + pid, status = Utils.execute_shell_with_log( build_command, @log.path ) end - # execute build script - build_root_dir = pkg_cl.location - env_def = - "BUILD_TARGET_OS=#{os} \ - SRCDIR=\"#{src_path}\" \ - ROOTDIR=\"#{build_root_dir}\" " - build_command = "cd \"#{src_path}\";" + env_def + "./package/build.#{@host_os} build" - if not Utils.execute_shell_with_log( build_command, @log ) then - @log.error( "Failed on build script", Log::LV_USER) + if not status.nil? and status.exitstatus != 0 then + @log.error( "Failed on build script: \"#{target}\"", Log::LV_USER) return false else + Utils.execute_shell( "rm -rf #{src_path}/.build.sh" ) return true end - end - # write pkginfo.manifest and install/remove script + # write pkginfo.manifest def write_pkginfo_files(pkginfo,os,src_path) - for pkg in pkginfo.packages + # get category + os_category = Utils.get_os_category( os ) + + pkginfo.packages.each do |pkg| # skip if not support the target os - if not pkg.os.include? os + if not pkg.os_list.include? os next end - # install script files - copy_post_install_script(pkg,os,src_path); - copy_post_remove_script(pkg,os,src_path); + # install/remove script files + if not copy_post_install_script(pkg,os,src_path) then return false end + if not copy_post_remove_script(pkg,os,src_path) then return false end # write manifest file install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" - + # if there is no intall directory, error if not File.exist? install_dir then - @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) - @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) - return false + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + @log.error( "Host OS name : #{@host_os}", Log::LV_USER) + @log.error( "It caused wrong pkginfo.manifest, pkginfo.manifest.local or build script", Log::LV_USER) + return false + end end - - # write pkginfo.manifest + + # write pkginfo.manifest File.open("#{install_dir}/pkginfo.manifest", "w") do |f| - pkg.print_to_file_with_os( f, os ) + f.puts pkg + f.puts "" + f.puts pkg.change_log_string end end @@ -494,74 +529,219 @@ class Builder # copy post-install script def copy_post_install_script(pkg,os,src_path) - + tar = nil + src = nil - if File.exist? "#{src_path}/package/#{pkg.package_name}.install.#{os}" - src = "#{src_path}/package/#{pkg.package_name}.install.#{os}" - else - src = nil + # get category + os_category_list = [] + pkg.os_list.each do |cos| + os_category_list.push Utils.get_os_category(cos) + end + + # check compatable os + (pkg.os_list + os_category_list).uniq.each do |cos| + if File.exist? "#{src_path}/package/#{pkg.package_name}.install.#{cos}" then + if src.nil? then + src = "#{src_path}/package/#{pkg.package_name}.install.#{cos}" + else + @log.error( "compatable package can have only one install script\n but you have another availabe install scripts", Log::LV_USER) + @log.error( " * package/#{File.basename src}", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.install.#{cos}", Log::LV_USER) + return false + end + end + end + + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" + + # if there is no intall directory, error + if not File.exist? install_dir then + os_category = Utils.get_os_category( os ) + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + @log.error( "Host OS name : #{@host_os}", Log::LV_USER) + @log.error( "It caused wrong pkginfo.manifest, pkginfo.manifest.local or build script", Log::LV_USER) + return false + end end if not src.nil? then - if os == "linux" or os == "darwin" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/install.sh" - elsif os == "windows" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/install.BAT" + if Utils.is_unix_like_os( os ) then + tar = "#{install_dir}/install.sh" + elsif Utils.is_windows_like_os( os) then + tar = "#{install_dir}/install.BAT" else puts "Unknown OS: #{os} " - return + return true end FileUtils.cp(src, tar) - end - - return + end + + return true end # copy post-remove script def copy_post_remove_script(pkg,os,src_path) - + tar = nil + src = nil - if File.exist? "#{src_path}/package/#{pkg.package_name}.remove.#{os}" - src = "#{src_path}/package/#{pkg.package_name}.remove.#{os}" - else - src = nil + # get category + os_category_list = [] + pkg.os_list.each do |cos| + os_category_list.push Utils.get_os_category(cos) + end + + # check compatable os + (pkg.os_list + os_category_list).uniq.each do |cos| + if File.exist? "#{src_path}/package/#{pkg.package_name}.remove.#{cos}" then + if src.nil? then + src = "#{src_path}/package/#{pkg.package_name}.remove.#{cos}" + else + @log.error( "compatable package can have only one remove script but you have another availabe remove scripts", Log::LV_USER) + @log.error( " * package/#{File.basename src}", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.remove.#{cos}", Log::LV_USER) + return false + end + end + end + + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" + + # if there is no intall directory, error + if not File.exist? install_dir then + os_category = Utils.get_os_category( os ) + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + @log.error( "Host OS name : #{@host_os}", Log::LV_USER) + @log.error( "It caused wrong pkginfo.manifest, pkginfo.manifest.local or build script", Log::LV_USER) + return false + end end if not src.nil? - puts "------> #{src}" - if os == "linux" or os == "darwin" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/remove.sh" - puts "-0--\==> #{tar}" - elsif os == "windows" then - tar = "#{src_path}/package/#{pkg.package_name}.package.#{os}/remove.BAT" + if Utils.is_unix_like_os( os ) then + tar = "#{install_dir}/remove.sh" + elsif Utils.is_windows_like_os( os) then + tar = "#{install_dir}/remove.BAT" else puts "Unknown OS: #{os} " - return + return true end FileUtils.cp(src, tar) - end + end + return true end # create package file def make_zip(pkginfo,os,src_path) - for pkg in pkginfo.packages + # get category + os_category = Utils.get_os_category( os ) + + pkginfo.packages.each do |pkg| # skip if not support the target os - if not pkg.os.include? os + if not pkg.os_list.include? os next end # cd install dir install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os}" - - # zip + if not File.exist? install_dir then + install_dir = "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + + if not File.exist? install_dir then + @log.error( "Following directory must be created before writing pkginfo.manifest", Log::LV_USER) + @log.error( " * package/#{pkg.package_name}.package.#{os}", Log::LV_USER) + @log.error( "Host OS name : #{@host_os}", Log::LV_USER) + @log.error( "It caused wrong pkginfo.manifest, pkginfo.manifest.local or build script", Log::LV_USER) + return false + end + end + + # zip @log.info( "Creating package file ... #{pkg.package_name}_#{pkg.version}_#{os}.zip", Log::LV_USER) - Utils.execute_shell("cd \"#{install_dir}\"; zip -r -y #{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip *") + cmd = "cd \"#{install_dir}\"; zip -r -y #{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip *" + @log.info( cmd ) + if not @job.nil? then + @job.execute_command( cmd ) + else + Utils.execute_shell_with_log(cmd, @log.path) + end + + if not File.exist? "#{src_path}/#{pkg.package_name}_#{pkg.version}_#{os}.zip" then + return false + end end + return true + end + + + # clean the temporary directory for packaged + def clean_project_directory(src_path, target_os = nil) + + # if os is not set, use host os instead + if target_os.nil? then target_os = @host_os end + + # convert path if windows + if Utils.is_windows_like_os(@host_os) then + build_root_dir = Utils.get_unix_path( @buildroot_dir ) + else + build_root_dir = @buildroot_dir + end + + # create pkginfo + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + + # get category + # make clean + pkginfo.packages.each do |pkg| + os = pkg.os + os_category = Utils.get_os_category( os ) + + if File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" + elsif File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + end + end + + # clean local-only package's directory + if File.exist? "#{src_path}/package/pkginfo.manifest.local" then + begin + pkginfo = PackageManifest.new("#{src_path}/package/pkginfo.manifest.local") + rescue => e + @log.error( e.message, Log::LV_USER) + return false + end + pkginfo.packages.each do |pkg| + os = pkg.os + os_category = Utils.get_os_category( os ) + + if File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{pkg.os}" + elsif File.exist? "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" then + FileUtils.rm_rf "#{src_path}/package/#{pkg.package_name}.package.#{os_category}" + end + end + end + + # execute + return execute_build_command("clean", src_path, build_root_dir, target_os, pkginfo.get_version) end end diff --git a/src/builder/CleanOptionParser.rb b/src/builder/CleanOptionParser.rb index 31611fc..7a2646b 100644 --- a/src/builder/CleanOptionParser.rb +++ b/src/builder/CleanOptionParser.rb @@ -1,6 +1,6 @@ =begin - - CleanOptionParser.rb + + CleanOptionParser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,20 +28,30 @@ Contributors: require 'optparse' require 'logger' +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils" def parse() - #option parsing - option = {} - optparse = OptionParser.new do |opts| - opts.banner = "Usage: pkg-clean" - opts.on('-h','--help', 'display this information') do - puts opts - exit - end - end - - optparse.parse! - - return option -end + #option parsing + option = {} + optparse = OptionParser.new(nil, 32, ' '*8) do |opts| + opts.banner = "Clean the package service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-clean [-h] [-v]" + "\n" \ + + "\n" + "Options:" + "\n" + + opts.on('-h','--help', 'display help') do + puts opts + exit + end + + opts.on('-v','--version', 'display version') do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end + end + + optparse.parse! + + return option +end diff --git a/src/builder/optionparser.rb b/src/builder/optionparser.rb index 05888f0..e8bab2c 100644 --- a/src/builder/optionparser.rb +++ b/src/builder/optionparser.rb @@ -1,5 +1,5 @@ =begin - + optionparser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,35 +28,53 @@ Contributors: require 'optparse' require 'logger' +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils" def parse() - #option parsing - option = {} - optparse = OptionParser.new do |opts| - opts.banner = "Usage: pkg-build -u -o -c -h" - opts.on('-u','--url ', 'remote package server url') do |url| - option[:url] = url - end - option[:os] = nil - opts.on('-o','--os ', 'operating system ') do |os| - option[:os] = os - end - option[:clean] = false - opts.on('-c','--clean', 'clean build') do - option[:clean] = true - end - option[:rev] = false - opts.on('-r','--rev', 'reverse build dependency check') do - option[:rev] = true - end - opts.on('-h','--help', 'display this information') do - puts opts - exit - end - end - - optparse.parse! - - return option -end + #option parsing + option = {} + optparse = OptionParser.new do |opts| + opts.banner = "Build and packaging service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-build -u [-o ] [-c] [-h] [-v]" + "\n" \ + + "\n" + "Options:" + "\n" + + opts.on('-u','--url ', 'remote package server url: http://127.0.0.1/dibs/unstable') do |url| + option[:url] = url + end + + option[:os] = nil + opts.on('-o','--os ', 'operating system ') do |os| + option[:os] = os + end + + option[:clean] = false + opts.on('-c','--clean', 'clean build') do + option[:clean] = true + end + + option[:rev] = false + #opts.on('-r','--rev', 'reverse build dependency check') do + # option[:rev] = true + #end + + opts.on('-h','--help', 'display help') do + puts opts + exit + end + + opts.on('-v','--version', 'display version') do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end + end + + optparse.parse! + + if option[:url].nil? or option[:url].empty? then + raise ArgumentError, "Usage: pkg-build -u [-o ] [-c] [-h]" + end + + return option +end diff --git a/src/common/Action.rb b/src/common/Action.rb new file mode 100644 index 0000000..ed18fb0 --- /dev/null +++ b/src/common/Action.rb @@ -0,0 +1,47 @@ +=begin + + Action.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +class Action + attr_accessor :time, :period + + def initialize( time, period ) + @time = time + @period = period + end + + + # initialize action + def init() + end + + + # execute action + def execute() + end + +end diff --git a/src/common/BuildComm.rb b/src/common/BuildComm.rb new file mode 100644 index 0000000..f80a9e2 --- /dev/null +++ b/src/common/BuildComm.rb @@ -0,0 +1,657 @@ +require "socket" + +=begin + + BuildComm.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(__FILE__) +require "log" +require 'timeout' +require "net/ftp" +require 'thread' +require "FileTransferViaFTP" +require "FileTransferViaDirect" + +ATTEMPTS = ["first", "second", "third"] + +class BuildCommServer + VERSION = "1.7.0" + + private_class_method :new + + def initialize(port, log, ftp_url, cache_dir) + @port = port + @log = log + @ftp_url = ftp_url + @cache_dir = cache_dir + @tcp_server = TCPServer.open( port ) + @download_cache_mutex = Mutex.new + end + + def self.create(port, log, ftp_url=nil, cache_dir=nil) + # checking port is available + if port_open? port then + raise "Port \"#{port}\" is already in use." + end + + if log.nil? then + log = DummyLog.new + end + + # create cache dir if not nil + if not cache_dir.nil? and not File.exist? cache_dir then + FileUtils.mkdir_p cache_dir + end + + return new(port, log, ftp_url, cache_dir) + end + + + # wait for connection and handle request + def wait_for_connection(quit_loop) + while( not quit_loop ) + req = @tcp_server.accept + + begin + yield req if block_given? + rescue + @log.error $! + @log.error "Caught a connection exception" + req.close + end + end + end + + + # terminate + def terminate + @tcp_server.close() + end + + + # send_begin + def self.send_begin( req ) + send( req, "=BEGIN,#{VERSION}") + end + + + def self.send_end( req ) + send( req, "=END") + end + + + def self.send_chk( req ) + send( req, "=CHK" ) + end + + + def self.send( req, line ) + begin + if not req.nil? then + req.puts line + end + rescue + raise "Connection is closed" + end + end + + + def send_file(req, src_file) + begin + if not File.exist? src_file then + @log.error "\"#{src_file}\" file does not exist" + req.puts "ERROR" + return false + end + + req.puts "READY" + @log.info "Ready to send file" + + while line = req.gets() + tok = line.split(",").map { |x| x.strip } + cmd = tok[0].strip + case cmd + when "CHECK_TRANSPORTER" + type = tok[1].strip + case type + when "DIRECT" + transporter = FileTransferDirect.new(@log) + when "FTP" + if not @ftp_url.nil? then + url_contents = Utils.parse_ftpserver_url(@ftp_url) + ip = url_contents[0] + port = url_contents[1] + username = url_contents[2] + passwd = url_contents[3] + transporter = FileTransferFTP.new(@log, ip, port, username, passwd) + else + transporter = FileTransferFTP.new(@log) + end + else + req.puts "ERROR" + @log.error "Unsupported transporter type! : #{type}" + return false + end + + req.puts "TRANSPORTER_OK" + + if not transporter.send_file( src_file, req, false ) then + return false + else + return true + end + + else + @log.warn "Unhandled message: #{line}" + end + end + + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + + # NOTE. dst_file can be directory + def receive_file(req, dst_file) + begin + req.puts "READY" + @log.info "Ready to receive file" + + while line = req.gets() + tok = line.split(",").map { |x| x.strip } + cmd = tok[0].strip + case cmd + when "CHECK_CACHE" + file_name = tok[1] + file_size = tok[2].to_i + checksum = tok[3] + + # check download cache + if File.exist? dst_file and File.directory? dst_file then + target_file = File.join(dst_file,file_name) + else + target_file = dst_file + end + if not @cache_dir.nil? and + check_download_cache( target_file, file_size, checksum ) then + + @log.info "Download cache hit! Copied from cache.: #{file_name}" + req.puts "CACHED" + else + @log.info "Cached file not found!#{file_name}" + req.puts "NOT_CACHED" + end + + when "CHECK_TRANSPORTER" + type = tok[1].strip + case type + when "DIRECT" + transporter = FileTransferDirect.new(@log) + when "FTP" + if not @ftp_url.nil? then + url_contents = Utils.parse_ftpserver_url(@ftp_url) + ip = url_contents[0] + port = url_contents[1] + username = url_contents[2] + passwd = url_contents[3] + transporter = FileTransferFTP.new(@log, ip, port, username, passwd) + else + transporter = FileTransferFTP.new(@log) + end + else + req.puts "ERROR" + @log.error "Unsupported transporter type! : #{type}" + return false + end + + req.puts "TRANSPORTER_OK" + + if not transporter.receive_file( dst_file, req, false ) then + return false + end + + # add to cache + if not @cache_dir.nil? then + add_download_cache(target_file) + end + break + + else + @log.warn "Unhandled message: #{line}" + end + end + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return true + end + + + def self.disconnect( req ) + begin + req.close + rescue + end + end + + def self.port_open?( port ) + Timeout::timeout(1) do + begin + TCPSocket.new("127.0.0.1",port).close + true + rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH + false + end + end + rescue Timeout::Error + false + end + + + private + def check_download_cache(dst_file, file_size, checksum ) + file_name = File.basename(dst_file) + cache_file = "#{@cache_dir}/#{file_name}" + + @download_cache_mutex.synchronize do + found = false + # check file exist + if File.exist? cache_file and + File.size(cache_file) == file_size and + Utils.checksum(cache_file) == checksum then + + # if hit , touch and copy + FileUtils.touch cache_file + FileUtils.copy_file(cache_file, dst_file) + + found = true + end + + # refresh cache dir + curr_time = Time.now + Dir.entries(@cache_dir).each do |fname| + if fname == "." or fname == ".." then next end + file_path = "#{@cache_dir}/#{fname}" + if File.mtime(file_path) + 3600 < curr_time then + FileUtils.rm_rf file_path + end + end + + return found + end + end + + + private + def add_download_cache(dst_file) + file_name = File.basename(dst_file) + cache_file = "#{@cache_dir}/#{file_name}" + @download_cache_mutex.synchronize do + # copy & touch + FileUtils.copy_file(dst_file, cache_file) + FileUtils.touch cache_file + end + end +end + + +class BuildCommClient + VERSION = "1.7.0" + FIRST_REPONSE_TIMEOUT = 120 + + private_class_method :new + + def initialize(socket, log) + @log = log + @socket = socket + @error_msg = "" + end + + + # create + # if sec 0 or nil then not set timeout. it's timeout spec + def self.create(ip, port, log = nil, sec = 5) + # open socket + socket = nil + begin + timeout(sec) do + socket = TCPSocket.open( ip, port ) + end + rescue Timeout::Error + return nil + rescue + # unknown exception + return nil + end + + # refused + if socket.nil? then + return nil + end + + if log.nil? then + log = DummyLog.new + end + + return new(socket, log) + end + + + def get_error_msg() + return @error_msg + end + + + def set_error_msg( msg ) + @error_msg = msg + end + + + def send( msg ) + if @socket.nil? then + @error_msg = "Connection is not available!" + return false + end + + @socket.puts( msg ) + return true + end + + + def print_stream + + begin + l = nil + timeout(FIRST_REPONSE_TIMEOUT) do + l = @socket.gets() + end + + if l.nil? then + @error_msg = "Connection closed or no more message" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @error_msg = "Comm. Protocol version is mismatched! #{VERSION}. Upgrade your DIBS client!" + return false + end + + # get contents + while line = @socket.gets() + if line.strip == "=END" then break end + if line.strip == "=CHK" then next end + # print + puts line.strip + end + rescue Timeout::Error + @error_msg = "Connection timed out!" + return false + + rescue => e + @error_msg = e.message + return false + end + + return true + end + + + # handle + def read_lines + + begin + # get first line + l = nil + timeout(FIRST_REPONSE_TIMEOUT) do + l = @socket.gets() + end + + if l.nil? then + @error_msg = "Connection closed or No more message" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @error_msg = "Comm. Protocol version is mismatched! #{VERSION}. Upgrade your DIBS client!" + return false + end + + # get contents + result = true + while line = @socket.gets() + if line.strip == "=END" then break end + if line.strip == "=CHK" then next end + + # execute + yield line.strip if block_given? + end + rescue Timeout::Error + @error_msg = "Connection timed out!" + return false + + rescue => e + @error_msg = e.message + return false + end + + return true + end + + + # return result + def receive_data + result = [] + + begin + l = nil + timeout(FIRST_REPONSE_TIMEOUT) do + l = @socket.gets() + end + + if l.nil? then + @error_msg = "Connection closed or No more message" + return nil + end + + # check protocol + if not protocol_matched? l.strip then + @error_msg = "Comm. Protocol version is mismatched! #{VERSION}. Upgrade your DIBS client!" + return nil + end + + # get contents + while line = @socket.gets() + if line.strip == "=END" then break end + if line.strip == "=CHK" then next end + # print + result.push line.strip + end + + rescue Timeout::Error + @error_msg = "Connection timed out!" + return nil + + rescue => e + @error_msg = e.message + return nil + end + + return result + end + + + def send_file(src_file, transporter ) + + result = true + begin + l = @socket.gets() + if l.nil? then + @log.error "[BuildCommClient] Connection refused" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @log.error "[BuildCommClient] Comm. Protocol version is mismatched! #{VERSION}. Upgrade your DIBS client!" + return false + end + + while line = @socket.gets() + cmd = line.split(",")[0].strip + case cmd + when "READY" + @log.info "Server is ready!" + file_name = File.basename(src_file) + file_size = File.size(src_file) + checksum = Utils.checksum(src_file) + send "CHECK_CACHE,#{file_name},#{file_size},#{checksum}" + + when "CACHED" + @log.info "Server already has cached file" + break + + when "NOT_CACHED" + @log.info "Server does not have cached file" + send "CHECK_TRANSPORTER,#{transporter.type}" + + when "TRANSPORTER_OK" + if not transporter.send_file( src_file, @socket, true ) then + result = false + else + @log.info "Sending file succeeded!" + end + + when "TRANSPORTER_FAIL" + @log.warn "Server does not support transporter type: #{transporter.type}" + result = false + + when "ERROR" + result = false + + when "=END" + break + + else + @log.warn "Unhandled message: #{line}" + end + end + rescue => e + puts "[BuildCommClient] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return result + end + + + # return file + def receive_file(dst_file, transporter) + result = true + + begin + l = @socket.gets() + + if l.nil? then + @log.error "[BuildCommClient] Connection refused" + return false + end + + # check protocol + if not protocol_matched? l.strip then + @log.error "[BuildCommClient] Comm. Protocol version is mismatched! #{VERSION}. Upgrade your DIBS client!" + return false + end + + while line = @socket.gets() + cmd = line.split(",")[0].strip + case cmd + when "READY" + @log.info "Server is ready!" + send "CHECK_TRANSPORTER,#{transporter.type}" + + when "TRANSPORTER_OK" + if not transporter.receive_file( dst_file, @socket, true ) then + result = false + else + @log.info "Receiving file succeeded!" + end + + when "ERROR" + result = false + + when "=END" + break + + else + @log.warn "Unhandled message: #{line}" + end + end + rescue => e + puts "[BuildCommServer] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return false + end + + return result + end + + + def terminate + @socket.close + end + + + private + + + # check protocol + def protocol_matched?(l) + + version = ( l.split(",")[1].nil? ? "1.0.0" : l.split(",")[1] ) + if not l.start_with? "=BEGIN" or + version.nil? or version != VERSION then + return false + else + return true + end + end +end diff --git a/src/common/FileTransferViaDirect.rb b/src/common/FileTransferViaDirect.rb new file mode 100644 index 0000000..d45f79e --- /dev/null +++ b/src/common/FileTransferViaDirect.rb @@ -0,0 +1,138 @@ +=begin + + FileTransferViaDirect.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'socket' +require 'log' + +class FileTransferDirect + attr_accessor :type + + def initialize(logger) + @type = "DIRECT" + + if not logger.nil? then + @log = logger + else + @log = DummyLog.new + end + end + + + def send_file( src_file, conn, is_client=true ) + + filename = File.basename(src_file) + size = File.size( src_file ) + checksum = Utils.checksum( src_file ) + + if is_client then + conn.puts "RECEIVE_REQ" + end + + while line = conn.gets() + tok = line.split(",") { |x| x.strip } + cmd = tok[0].strip + case cmd + when "SEND_REQ" + conn.puts "FILE_INFO,#{filename},#{size},#{checksum}" + # read file contents + # send via tcp/ip + File.open(src_file, "rb") do |io| + while size > 0 + buf = io.read(size > 1024*1024 ? 1024*1024 : size) + conn.write( buf ) + size -= buf.length + end + end + + @log.info "Upload is succeeded!" + conn.puts "SEND_OK" + + # wait for download result + when "RECEIVE_OK" + @log.info "Received download success message from remote site" + return true + + when "RECEIVE_FAIL" + @log.info "Received download fail message from remote site" + return false + + else + @log.error "Unhandled message: #{line}" + return false + end + end + end + + + def receive_file( dst_file, conn, is_client=false ) + + if is_client then + conn.puts "SEND_REQ" + end + + while line = conn.gets() + tok = line.split(",") { |x| x.strip } + cmd = tok[0].strip + case cmd + when "RECEIVE_REQ" + conn.puts "SEND_REQ" + when "FILE_INFO" + @log.info "Received file info from remote site" + filename = tok[1].strip + size = tok[2].strip.to_i + checksum = tok[3].strip + + if File.directory? dst_file then + dst_file = File.join(dst_file, filename) + end + + File.open( dst_file, "wb" ) do |io| + while size > 0 + buf = conn.read(size > 1024*1024 ? 1024*1024 : size) + if buf.nil? then + @log.error "Reading data from connection failed!" + return false + end + io.write( buf ) + size -= buf.length + end + end + + conn.puts "RECEIVE_OK" + + when "SEND_OK" + @log.info "Received success message from remote site" + return true + + else + @log.error "Unhandled message: #{line}" + return false + end + end + end +end diff --git a/src/common/FileTransferViaFTP.rb b/src/common/FileTransferViaFTP.rb new file mode 100644 index 0000000..71a14d0 --- /dev/null +++ b/src/common/FileTransferViaFTP.rb @@ -0,0 +1,288 @@ +=begin + + FileTransferViaFTP.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'socket' +require 'log' + +class FileTransferFTP + attr_accessor :type + ATTEMPTS = ["first", "second", "third"] + + def initialize(logger, ip=nil, port=nil, username=nil, passwd=nil ) + @type = "FTP" + @ip = ip + @port = port + @username = username + @passwd = passwd + if not logger.nil? then + @log = logger + else + @log = DummyLog.new + end + end + + + def send_file( src_file, conn, is_client=true ) + + if is_client then + # check ftp info + if @ip.nil? or @port.nil? or @username.nil? or @passwd.nil? then + @log.error "No FTP information!" + conn.puts "UPLOAD_FAIL" + return false + end + conn.puts "DOWNLOAD_REQ,#{@ip},#{@port},#{@username},#{@passwd}" + end + + ip = @ip; port = @port; username = @username; passwd = @passwd + while line = conn.gets() + tok = line.split(",") { |x| x.strip } + cmd = tok[0].strip + case cmd + when "UPLOAD_REQ" + if @ip.nil? or @port.nil? or @username.nil? or @passwd.nil? then + ip = tok[1].strip + port = tok[2].strip + username = tok[3].strip + passwd = tok[4].strip + @log.info "Using FTP information from remote... [#{ip}, #{port}]" + end + + # upload to ftp + ftp_filepath = nil + for attempt in ATTEMPTS + ftp_filepath = putfile( src_file, ip, port, username, passwd ) + if !ftp_filepath.nil? then + break + else + @log.info "The #{attempt} uploading attempt failed!" + end + end + + if ftp_filepath.nil? then + conn.puts "UPLOAD_FAIL" + return false + else + @log.info "Upload is succeeded at #{attempt}" + conn.puts "UPLOAD_OK,#{ftp_filepath}" + end + + # wait for download result + when "DOWNLOAD_OK" + @log.info "Received download success message from remote site" + # clean + cleandir( ftp_filepath, ip, port, username, passwd) + @log.info "Cleaned temporary dir on FTP server: #{ftp_filepath}" + return true + + when "DOWNLOAD_FAIL" + @log.info "Received download fail message from remote site" + return false + + else + @log.error "Unhandled message: #{line}" + return false + end + end + end + + + def receive_file( dst_file, conn, is_client=false ) + + if is_client then + # check ftp info + if @ip.nil? or @port.nil? or @username.nil? or @passwd.nil? then + @log.error "No FTP information!" + conn.puts "DOWNLOAD_FAIL" + return false + end + conn.puts "UPLOAD_REQ,#{@ip},#{@port},#{@username},#{@passwd}" + end + + ip = @ip; port = @port; username = @username; passwd = @passwd + while line = conn.gets() + tok = line.split(",") { |x| x.strip } + cmd = tok[0].strip + case cmd + when "DOWNLOAD_REQ" + if @ip.nil? or @port.nil? or @username.nil? or @passwd.nil? then + ip = tok[1].strip + port = tok[2].strip + username = tok[3].strip + passwd = tok[4].strip + @log.info "Using FTP information from remote... [#{ip}, #{port}]" + end + + conn.puts "UPLOAD_REQ,#{ip},#{port},#{username},#{passwd}" + when "UPLOAD_OK" + @log.info "Received upload success message from remote site" + filepath = tok[1].strip + # download from ftp + dst_filepath = nil + for attempt in ATTEMPTS + dst_filepath = getfile( filepath, dst_file, ip, port, username, passwd ) + if not dst_filepath.nil? then + break + else + @log.info "The #{attempt} downloading attempt failed!" + end + end + if dst_filepath.nil? then + conn.puts "DOWNLOAD_FAIL" + return false + else + @log.info " Server is the #{attempt} successful attempt to download" + conn.puts "DOWNLOAD_OK" + return true + end + + when "UPLOAD_FAIL" + @log.info "Received upload fail message from remote site" + return false + + else + @log.error "Unhandled message: #{line}" + return false + end + end + end + + + def putfile( bpath, ip, port, username, passwd ) + filename = File.basename(bpath) + uniqdir = Utils.create_uniq_name + ftp_filepath = File.join(uniqdir, filename) + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + @log.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + ftp.binary = true + ftp.passive = true + ftp.mkdir(uniqdir) + ftp.chdir(uniqdir) + ftp.put(bpath) + @log.info "[FTP log] Put a file" + @log.info "[FTP log] from \"#{bpath}\" to \"#{ftp_filepath}\"" + files = ftp.list(filename) + if files.empty? then + @log.error "[FTP log] Failed to upload file (#{filename} does not exist)" + return nil + end + ftp.quit + @log.info "[FTP log] Disconnected FTP server" + rescue => e + @log.error "[FTP log] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return nil + end + return ftp_filepath + end + + def getfile( bpath, target, ip, port, username, passwd ) + dirname = File.dirname(bpath) + filename = File.basename(bpath) + + # target can be directory or file + if File.directory? target then + dst_file = File.join(target,filename) + else + dst_file = target + end + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + @log.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + ftp.binary = true + ftp.passive = true + ftp.chdir(dirname) + ftp.get(filename, dst_file) + @log.info "[FTP log] Get a file" + @log.info "[FTP log] from \"#{bpath}\" to \"#{dst_file}\"" + ftp.quit + @log.info "[FTP log] Disconnected FTP server" + rescue => e + @log.error "[FTP log] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return nil + end + if not File.exist? dst_file then + @log.error "[FTP log] Failed to download file (#{dst_file} does not exist)" + return nil + end + return bpath + end + + def cleandir(path, ip, port, username, passwd) + dirname = File.dirname(path) + + begin + ftp = Net::FTP.new + if port.nil? or port == "" then + ftp.connect(ip) + else + ftp.connect(ip, port) + end + @log.info "[FTP log] Connected FTP server (#{ip}:#{port})" + ftp.login(username, passwd) + old_dir = ftp.pwd + ftp.chdir(dirname) + list = ftp.ls + # TODO: if list is directory? + list.each do |l| + file = l.split(" ")[-1].strip + ftp.delete(file) + end + ftp.chdir(old_dir) + ftp.rmdir(dirname) + @log.info "[FTP log] Clean dir (#{dirname})" + ftp.quit + @log.info "[FTP log] Disconnected FTP server" + rescue => e + @log.error "[FTP log] Exception" + @log.error e.message + @log.error e.backtrace.inspect + return nil + end + + return true + end +end diff --git a/src/common/PackageManifest.rb b/src/common/PackageManifest.rb index 4a9c2d3..c946dd2 100644 --- a/src/common/PackageManifest.rb +++ b/src/common/PackageManifest.rb @@ -1,5 +1,5 @@ =begin - + PackageManifest.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -33,45 +33,38 @@ class PackageManifest attr_accessor :packages def initialize( file_path ) - @pkg_map = Parser.read_pkginfo_list( file_path ) - @packages = @pkg_map.values + @packages = Parser.read_multy_pkginfo_from file_path end # scan all build dependencies - def get_build_dependencies( target_os, host_os ) + def get_build_dependencies( target_os ) # for all list = [] - for pkg in @packages + @packages.each do |pkg| # package that has the target os - if not pkg.os.include?(target_os) + if not pkg.os_list.include?(target_os) next end - # package that has the host os - if not pkg.build_host_os.include?(host_os) - next - end # package that has the target os - for dep in pkg.build_dep_list - # if dep.target_os_list.include? target_os - list.push dep - # end + pkg.build_dep_list.each do |dep| + list.push dep end end list.uniq! - + return list - end + end - - # scan all build dependencies + + # scan all source dependencies def get_source_dependencies( target_os, host_os ) # for all list = [] - for pkg in @packages + @packages.each do |pkg| # only package that used in target os - if not pkg.os.include?(target_os) + if not pkg.os_list.include?(target_os) next end @@ -81,27 +74,79 @@ class PackageManifest end # package that has the target os - for dep in pkg.source_dep_list - # if dep.target_os_list.include? target_os - list.push dep - # end + pkg.source_dep_list.each do |dep| + # if dep.target_os_list.include? target_os + list.push dep + # end + end + end + list.uniq! + + return list + end + + + # scan all install dependencies + def get_install_dependencies( target_os, pkg_name=nil ) + # for all + list = [] + @packages.each do |pkg| + if not pkg_name.nil? and pkg.package_name != pkg_name then next end + # only package that used in target os + if not pkg.os_list.include?(target_os) + next + end + + # package that has the target os + pkg.install_dep_list.each do |dep| + list.push dep end end list.uniq! return list - end + end def package_exist?(target_os, host_os) - for pkg in @packages + @packages.each do |pkg| # only package that used in target os - if pkg.os.include?(target_os) and + if pkg.os_list.include?(target_os) and pkg.build_host_os.include?(host_os) return true end end return false - end + end + + + def get_version() + return @packages[0].version + end + + + def get_target_packages(target_os) + pkgs = [] + @packages.each do |pkg| + if pkg.os_list.include?(target_os) then + pkgs.push pkg + end + end + + return pkgs + end + + + def pkg_exist?(name,ver,os) + @packages.each do |pkg| + if pkg.package_name != name then next end + if not ver.nil? and pkg.version != ver then next end + if not os.nil? and not pkg.os_list.include?(os) then next end + + return true + end + + return false + end end diff --git a/src/common/ScheduledActionHandler.rb b/src/common/ScheduledActionHandler.rb new file mode 100644 index 0000000..edbd691 --- /dev/null +++ b/src/common/ScheduledActionHandler.rb @@ -0,0 +1,117 @@ +=begin + + ScheduledActionHandler.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(__FILE__) +require 'thread' + +class ScheduledActionHandler + attr_accessor :quit, :thread + + # init + def initialize( ) + @thread = nil + @quit = false + @actions = [] + @mutex = Mutex.new + end + + + # register a action + def register( action ) + # init action + action.init + # add to list + @mutex.synchronize do + @actions.push action + end + end + + + # unregsister a action + def unregister( action ) + @mutex.synchronize do + @actions.delete(action) + end + end + + + # get all actions + def get_actions() + return @actions + end + + + # start thread + def start() + @thread = Thread.new do + # main + thread_main() + + # close + terminate() + end + end + + + protected + + def thread_main + + while not @quit + + current_time = Time.new + + # get list + action_list = Array.new(@actions) + action_list.each do |action| + # if its time is reached, execute action + if not action.time.nil? and current_time > action.time then + action.execute + + # if periodic action, renew the time + # else remove it from list + if action.period != 0 then + while current_time > action.time + action.time = action.time + action.period + end + else + unregister(action) + end + end + end + + # sleep 10 sec + sleep 10 + end + end + + + def terminate + end + +end diff --git a/src/common/Version.rb b/src/common/Version.rb index 3bdba12..7d7921d 100644 --- a/src/common/Version.rb +++ b/src/common/Version.rb @@ -1,5 +1,5 @@ =begin - + Version.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,22 +28,11 @@ Contributors: class Version < Array - def initialize s - super(s.split('.').map { |e| e.to_i }) - end - def < x - (self <=> x) < 0 - end - def <= x - (self <=> x) <= 0 - end - def > x - (self <=> x) > 0 - end - def >= x - (self <=> x) >= 0 - end - def == x - (self <=> x) == 0 - end -end + include Comparable + def initialize s + super(s.split('.').map { |e| e.to_i }) + end + def compare x + self <=> x + end +end diff --git a/src/common/dependency.rb b/src/common/dependency.rb index a6e9499..74b5ae6 100644 --- a/src/common/dependency.rb +++ b/src/common/dependency.rb @@ -1,5 +1,5 @@ =begin - + dependency.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -27,31 +27,31 @@ Contributors: =end $LOAD_PATH.unshift File.dirname(__FILE__) -require "Version" +require "Version" class Dependency - attr_accessor :package_name, :comp, :base_version, :target_os_list - def initialize (package_name, comp, base_version, target_os_list) - @package_name = package_name - @comp = comp - @base_version = base_version - @target_os_list = target_os_list - end + attr_accessor :package_name, :comp, :base_version, :target_os_list + def initialize (package_name, comp, base_version, target_os_list) + @package_name = package_name + @comp = comp + @base_version = base_version + @target_os_list = target_os_list + end - def to_s - string = @package_name - if not @comp.nil? and not @base_version.nil? then - string = string + " ( #{@comp} #{@base_version} )" - end - - if not @target_os_list.empty? then - string = string + " [ #{@target_os_list.join("|")} ]" - end - return string - end + def to_s + string = @package_name + if not @comp.nil? and not @base_version.nil? then + string = string + " ( #{@comp} #{@base_version} )" + end + + if not @target_os_list.empty? then + string = string + " [ #{@target_os_list.join("|")} ]" + end + return string + end def match? ver - if @base_version.nil? + if @base_version.nil? return true end @@ -68,7 +68,7 @@ class Dependency return Version.new(ver) < Version.new(@base_version) else return true - end + end end -end +end diff --git a/src/common/execute_with_log.rb b/src/common/execute_with_log.rb new file mode 100755 index 0000000..58cb982 --- /dev/null +++ b/src/common/execute_with_log.rb @@ -0,0 +1,56 @@ +#!/usr/bin/ruby +=begin + + execute_with_log.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'logger' +$LOAD_PATH.unshift File.dirname(__FILE__) +require 'utils.rb' + +# parse arguments +cmd = ARGV[0] +log_path = ARGV[1] + +# create logger +if log_path.nil? or log_path.empty? then + log = DummyLog.new +else + log = Logger.new(log_path) +end + +# generate command +cmd = Utils.generate_shell_command(cmd, nil) + +# execute and write log +IO.popen("#{cmd} 2>&1") do |io| + io.each do |line| + log.info line + end +end + +# return exit code +exit $?.exitstatus diff --git a/src/common/log.rb b/src/common/log.rb index afa8fd7..893885f 100644 --- a/src/common/log.rb +++ b/src/common/log.rb @@ -1,5 +1,5 @@ =begin - + log.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -30,7 +30,7 @@ require "logger" class Log - attr_accessor :path + attr_accessor :path, :cnt # Log LEVEL LV_NORMAL = 1 @@ -39,7 +39,8 @@ class Log # init def initialize(path, lv=LV_USER) - @path = path + @cnt = 0 + @path = path if @path.nil? then @logger = Logger.new(STDOUT) else @@ -52,43 +53,47 @@ class Log # diable logger format @default_formatter = @logger.formatter @no_prefix_formatter = proc do |severity, datetime, progname, msg| - " >#{msg}" + " >#{msg}" end end def info(msg, lv=LV_NORMAL) - if @path.nil? then puts "Info: #{msg}" - else @logger.info msg end - if not @second_out.nil? and lv >= @second_out_level then - output_extra "Info: " + msg + if @path.nil? and not @second_out.nil? then puts "Info: #{msg}" + else @logger.info msg end + if not @second_out.nil? and lv >= @second_out_level then + output_extra "Info: " + msg end - end + @cnt = @cnt + 1 + end - def warn(msg, lv=LV_NORMAL) - if @path.nil? then puts "Warn: #{msg}" + def warn(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts "Warn: #{msg}" else @logger.warn msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra "Warn: " + msg + output_extra "Warn: " + msg end - end + @cnt = @cnt + 1 + end - def error(msg, lv=LV_NORMAL) - if @path.nil? then puts "Error: #{msg}" + def error(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts "Error: #{msg}" else @logger.error msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra "Error: " + msg + output_extra "Error: " + msg end + @cnt = @cnt + 1 end - def output(msg, lv=LV_NORMAL) - if @path.nil? then puts msg + def output(msg, lv=LV_NORMAL) + if @path.nil? and not @second_out.nil? then puts msg else @logger.info msg end if not @second_out.nil? and lv >= @second_out_level then - output_extra msg + output_extra msg end + @cnt = @cnt + 1 end @@ -96,10 +101,47 @@ class Log @second_out= nil end + def is_verbose + return @second_out_level.eql? Log::LV_NORMAL + end + protected def output_extra(msg) - #do nothing - end + #do nothing + end +end + +class DummyLog + attr_accessor :path, :cnt + def initialize() + @path = "" + @cnt = 0 + end + def info(str, lv=nil) + puts "I, [#{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}] INFO -- : " + str + end + def error(str, lv=nil) + puts "E, [#{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}] ERROR -- : " + str + end + def warn(str, lv=nil) + puts "W, [#{Time.now.strftime("%Y-%m-%dT%H:%M:%S")}] WARN -- : " + str + end + def output(str, lv=nil) + puts "" + str + end +end + +class StandardOutLog < Log + + def initialize(path) + super(path) + @second_out = $stdout + end + + protected + def output_extra(msg) + @second_out.puts msg + end end diff --git a/src/common/mail.rb b/src/common/mail.rb index a49c1d2..aedc753 100644 --- a/src/common/mail.rb +++ b/src/common/mail.rb @@ -1,5 +1,5 @@ =begin - + mail.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -30,15 +30,15 @@ require 'net/smtp' $LOAD_PATH.unshift File.dirname(__FILE__) require "mailConfig" -class Mail +class Mail - def Mail.send_mail( mail_to, subject, contents ) + def Mail.send_mail( mail_to, subject, contents ) if mail_to.nil? or mail_to.empty? \ - or subject.nil? or subject.empty? \ - or contents.nil? or contents.empty? then + or subject.nil? or subject.empty? \ + or contents.nil? or contents.empty? then return false - end + end message = < e + begin + Net::SMTP.start('localhost') do |smtp| + smtp.send_message( message, SENDER, mail_to_list) + end + rescue => e puts "Can't send result email" puts e.message end @@ -69,18 +69,18 @@ MESSAGE_END def Mail.parse_email( low_email_list ) mail_list = [] - low_email_list.each do | low_email | + low_email_list.split(",").each do | low_email | ms = low_email.index('<') - me = low_email.index('>') - if ms.nil? or me.nil? then - next - else - mail = low_email[(ms+1)..(me-1)] - end - - if mail.include?("@") then mail_list.push mail end - end + me = low_email.index('>') + if ms.nil? or me.nil? then + next + else + mail = low_email[(ms+1)..(me-1)] + end + + if mail.include?("@") then mail_list.push mail end + end - return mail_list + return mail_list end end diff --git a/src/common/package.rb b/src/common/package.rb index 122f977..a7c139f 100644 --- a/src/common/package.rb +++ b/src/common/package.rb @@ -1,6 +1,6 @@ =begin - - package.rb + + package.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -27,135 +27,88 @@ Contributors: =end class Package - attr_accessor :package_name, :version, :os, :build_host_os, :maintainer, :attribute, :install_dep_list, :build_dep_list, :source_dep_list, :conflicts, :source, :src_path, :path, :origin, :checksum, :size, :description - def initialize (package_name) - @package_name = package_name - @version = "" - @os = "" - @build_host_os = [] - @maintainer = "" - @attribute = [] - @install_dep_list = [] - @build_dep_list = [] - @source_dep_list = [] - @conflicts = [] - @source = "" - @src_path = "" - @path = "" - @origin = "" - @checksum = "" - @size = "" - @description = "" - end - def print - puts "Package : " + @package_name - if not @version.empty? then puts "Version : " + @version end - if not @os.empty? then puts "OS : " + @os end - if not @build_host_os.empty? then puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then puts "Maintainer : " + @maintainer end - if not @attribute.empty? then puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then puts "Source : " + @source end - if not @src_path.empty? then puts "Src-path : " + @src_path end - if not @path.empty? then puts "Path : " + @path end - if not @origin.empty? then puts "Origin : " + @origin end - if not @checksum.empty? then puts "SHA256 : " + @checksum end - if not @size.empty? then puts "Size : " + @size end - if not @description.empty? then puts "Description : " + @description end - end + attr_accessor :package_name, :label, :version, :os, :build_host_os, :maintainer, :attribute, :install_dep_list, :build_dep_list, :source_dep_list, :conflicts, :source, :src_path, :path, :origin, :checksum, :size, :description, :os_list, :custom, :change_log + def initialize (package_name) + @package_name = package_name + @label = "" + @version = "" + @os = "" + @os_list = [] + @build_host_os = [] + @maintainer = "" + @attribute = [] + @install_dep_list = [] + @build_dep_list = [] + @source_dep_list = [] + @conflicts = [] + @source = "" + @src_path = "" + @path = "" + @origin = "" + @checksum = "" + @size = "" + @description = "" + @custom = "" + @change_log = {} + end + + def print + puts self.to_s + end + + def to_s + string = "Package : " + @package_name + if not @label.empty? then string = string + "\n" + "Label : " + @label end + if not @version.empty? then string = string + "\n" + "Version : " + @version end + if not @os_list.empty? then string = string + "\n" + "OS : " + @os_list.join(", ") end + if not @build_host_os.empty? then string = string + "\n" + "Build-host-os : " + @build_host_os.join(", ") end + if not @maintainer.empty? then string = string + "\n" + "Maintainer : " + @maintainer end + if not @attribute.empty? then string = string + "\n" + "Attribute : " + @attribute.join("|") end + if not @install_dep_list.empty? then + string = string + "\n" + "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") + end + if not @build_dep_list.empty? then + string = string + "\n" + "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") + end + if not @source_dep_list.empty? then + string = string + "\n" + "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") + end + if not @conflicts.empty? then + string = string + "\n" + "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") + end + if not @source.empty? then string = string + "\n" + "Source : " + @source end + if not @src_path.empty? then string = string + "\n" + "Src-path : " + @src_path end + if not @path.empty? then string = string + "\n" + "Path : " + @path end + if not @origin.empty? then string = string + "\n" + "Origin : " + @origin end + if not @checksum.empty? then string = string + "\n" + "SHA256 : " + @checksum end + if not @size.empty? then string = string + "\n" + "Size : " + @size end + if not @custom.empty? then string = string + "\n" + @custom end + if not @description.empty? then string = string + "\n" + "Description : " + @description end + return string + end + + def print_to_file(file) + file.puts self.to_s + end + + def change_log_string + if @change_log.empty? then return "" end + + string = "" + @change_log.sort.each do |list| + string = "* " + list[0] + "\n" + list[1] + "\n" + string + end + return "#Change log\n" + string + end - def to_s - string = "Package : " + @package_name - if not @version.empty? then string = string + "\n" + "Version : " + @version end - if not @os.empty? then string = string + "\n" + "OS : " + @os end - if not @build_host_os.empty? then string = string + "\n" + "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then string = string + "\n" + "Maintainer : " + @maintainer end - if not @attribute.empty? then string = string + "\n" + "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - string = string + "\n" + "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - string = string + "\n" + "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - string = string + "\n" + "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - string = string + "\n" + "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then string = string + "\n" + "Source : " + @source end - if not @src_path.empty? then string = string + "\n" + "Src-path : " + @src_path end - if not @path.empty? then string = string + "\n" + "Path : " + @path end - if not @origin.empty? then string = string + "\n" + "Origin : " + @origin end - if not @checksum.empty? then string = string + "\n" + "SHA256 : " + @checksum end - if not @size.empty? then string = string + "\n" + "Size : " + @size end - if not @description.empty? then string = string + "\n" + "Description : " + @description end - return string - end - def print_to_file(file) - file.puts "Package : " + @package_name - if not @version.empty? then file.puts "Version : " + @version end - if not @os.empty? then file.puts "OS : " + @os end - if not @build_host_os.empty? then file.puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then file.puts "Maintainer : " + @maintainer end - if not @attribute.empty? then file.puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - file.puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - file.puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - file.puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - file.puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then file.puts "Source : " + @source end - if not @src_path.empty? then file.puts "Src-path : " + @src_path end - if not @path.empty? then file.puts "Path : " + @path end - if not @origin.empty? then file.puts "Origin : " + @origin end - if not @checksum.empty? then file.puts "SHA256 : " + @checksum end - if not @size.empty? then file.puts "Size : " + @size end - if not @description.empty? then file.puts "Description : " + @description end - end + def does_change_exist? + if not @change_log.empty? and not @change_log[@version].nil? then + return true + end + return false + end - def print_to_file_with_os(file,target_os) - file.puts "Package : " + @package_name - if not @version.empty? then file.puts "Version : " + @version end - file.puts "OS : " + target_os - if not @build_host_os.empty? then file.puts "Build-host-os : " + @build_host_os.join("|") end - if not @maintainer.empty? then file.puts "Maintainer : " + @maintainer end - if not @attribute.empty? then file.puts "Attribute : " + @attribute.join("|") end - if not @install_dep_list.empty? then - file.puts "Install-dependency : " + @install_dep_list.map {|x| x.to_s}.join(", ") - end - if not @build_dep_list.empty? then - file.puts "Build-dependency : " + @build_dep_list.map {|x| x.to_s}.join(", ") - end - if not @source_dep_list.empty? then - file.puts "Source-dependency : " + @source_dep_list.map {|x| x.to_s}.join(", ") - end - if not @conflicts.empty? then - file.puts "Conflicts : " + @conflicts.map {|x| x.to_s}.join(", ") - end - if not @source.empty? then file.puts "Source : " + @source end - if not @src_path.empty? then file.puts "Src-path : " + @src_path end - if not @path.empty? then file.puts "Path : " + @path end - if not @origin.empty? then file.puts "Origin : " + @origin end - if not @checksum.empty? then file.puts "SHA256 : " + @checksum end - if not @size.empty? then file.puts "Size : " + @size end - if not @description.empty? then file.puts "Description : " + @description end - end -end + def get_changes + return @change_log[@version] + end +end diff --git a/src/common/parser.rb b/src/common/parser.rb index 01f7acf..87949e0 100644 --- a/src/common/parser.rb +++ b/src/common/parser.rb @@ -1,6 +1,6 @@ =begin - - parser.rb + + parser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -31,209 +31,290 @@ require "package" require "dependency" class Parser - def Parser.read_pkginfo_list (file) - pkglist = {} - File.open file,"r" do |f| - #variable initialize - package_name = "" - version = "" - os = "" - build_host_os = [] - maintainer = "" - attribute = [] - install_dep_list = [] - build_dep_list = [] - source_dep_list = [] - conflicts = [] - source = "" - src_path = "" - path = "" - origin = "" - checksum = "" - size = "" - description = "" - - f.each_line do |l| - # separator - if l.strip.empty? then - #make package and initialize - if not package_name.empty? and not os.empty? then - package = Package.new(package_name) - if not version.empty? then package.version = version end - if not os.empty? then package.os = os end - if not build_host_os.empty? then package.build_host_os = build_host_os end - if not maintainer.empty? then package.maintainer = maintainer end - if not attribute.empty? then package.attribute = attribute end - if not install_dep_list.empty? then package.install_dep_list = install_dep_list end - if not build_dep_list.empty? then package.build_dep_list = build_dep_list end - if not source_dep_list.empty? then package.source_dep_list = source_dep_list end - if not conflicts.empty? then package.conflicts = conflicts end - if not source.empty? then package.source = source end - if not src_path.empty? then package.src_path = src_path end - if not path.empty? then package.path = path end - if not origin.empty? then package.origin = origin end - if not checksum.empty? then package.checksum = checksum end - if not size.empty? then package.size = size end - if not description.empty? then package.description = description end - pkglist[[package_name,os]] = package - package_name = "" - version = "" - os = "" - bulid_host_os = [] - maintainer = "" - attribute = [] - install_dep_list = [] - build_dep_list = [] - source_dep_list = [] - conflicts = [] - source = "" - src_path = "" - path = "" - origin = "" - checksum = "" - size = "" - description = "" - end - next - end - # commant - if l.strip.start_with? "#" then next end - #contents - dsic_on = false - case l.strip.split(':')[0].strip - when /^Package/i then - package_name = l.sub(/^[ \t]*Package[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Version/i then - version = l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^OS/i then - os = l.sub(/^[ \t]*OS[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Build-host-os/i then - build_host_os = l.sub(/^[ \t]*Build-host-os[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split("|") - disc_on=false - when /^Maintainer/i then - maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Attribute/i then - attribute = l.sub(/^[ \t]*Attribute[ \t]*:[ \t]*/i,"").tr(" \t\n\r","").split("|") - disc_on=false - when /^Install-dependency/i then - install_dep_list = dep_parser l.sub(/^[ \t]*Install-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Build-dependency/i then - build_dep_list = dep_parser l.sub(/^[ \t]*Build-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Source-dependency/i then - source_dep_list = dep_parser l.sub(/^[ \t]*Source-dependency[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Conflicts/i then - conflicts = dep_parser l.sub(/^[ \t]*Conflicts[ \t]*:[ \t]*/i,"").split(',') - disc_on=false - when /^Source/i then - source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Src-path/i then - src_path = l.sub(/^[ \t]*Src-path[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Path/i then - path = l.sub(/^[ \t]*Path[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Origin/i then - origin = l.sub(/^[ \t]*Origin[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^SHA256/i then - checksum = l.sub(/^[ \t]*SHA256[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Size/i then - size = l.sub(/^[ \t]*Size[ \t]*:[ \t]*/i,"").strip - disc_on=false - when /^Description/i then - description = l.sub(/^[ \t]*Description[ \t]*:[ \t]*/i,"") - disc_on=true - else - if disc_on then - description = description + l - else - puts "unknown section : #{l}" - end - end - - end - #i essent - - # check last package - if not package_name.empty? and not os.empty? then - package = Package.new(package_name) - if not version.empty? then package.version = version end - if not os.empty? then package.os = os end - if not build_host_os.empty? then package.build_host_os = build_host_os end - if not maintainer.empty? then package.maintainer = maintainer end - if not attribute.empty? then package.attribute = attribute end - if not install_dep_list.empty? then package.install_dep_list = install_dep_list end - if not build_dep_list.empty? then package.build_dep_list = build_dep_list end - if not source_dep_list.empty? then package.source_dep_list = source_dep_list end - if not conflicts.empty? then package.conflicts = conflicts end - if not source.empty? then package.source = source end - if not src_path.empty? then package.src_path = src_path end - if not path.empty? then package.path = path end - if not origin.empty? then package.origin = origin end - if not checksum.empty? then package.checksum = checksum end - if not size.empty? then package.size = size end - if not description.empty? then package.description = description end - pkglist[[package_name,os]] = package - end - end - return pkglist - end - - def Parser.read_pkginfo (file) - return read_pkg_list(file).values[0] - end - - def Parser.read_pkg_list (file) - result = {} - read_pkginfo_list(file).values.each { |x| result[x.package_name]=x } - return result - end - - private - def Parser.dep_parser (string_list) - dependency_list = [] - string_list.each do |dep| - #variable initialize - package_name = "" - comp = nil - base_version = nil - target_os_list = [] - #string trim - dependency = dep.tr " \t\n", "" - #version extract - vs = dependency.index('(') - ve = dependency.index(')') - if not vs.nil? and not ve.nil? then - comp = dependency[(vs+1)..(vs+2)] - base_version = dependency[(vs+3)..(ve-1)] - end - #os list extract - os = dependency.index('[') - oe = dependency.index(']') - if not os.nil? and not oe.nil? then - target_os_list = dependency[(os+1)..(oe-1)].split("|") - end - # package_name extract - pe = dependency.index(/[\]\[\)\(]/) - if pe.nil? - package_name = dependency - else - package_name = dependency[0..pe-1] - end - #package_name check - if not package_name.empty? then - dependency_list.push Dependency.new(package_name,comp,base_version,target_os_list) - end - end - return dependency_list - end -end + def Parser.read_multy_pkginfo_from (file, only_common = false) + pkglist = [] + package = nil + common_source = "" + common_version = "" + common_maintainer = "" + change_log = {} + multi_line = nil + change_version = nil + change_contents = "" + + #file check + + File.open file,"r" do |f| + #variable initialize + state = "INIT" + f.each_line do |l| + # commant + if l.strip.start_with? "#" then next end + + field_name = l.split(':')[0].strip + + case state + when "INIT" then + case field_name + when /^$/ then next + when /^Source$/i then + state = "COMMON" + if common_source.empty? then common_source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^Version$/i then + state = "COMMON" + if common_version.empty? then common_version = check_version l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^Maintainer$/i then + state = "COMMON" + if common_maintainer.empty? then common_maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^Package$/i then + state = "PACKAGE" + package_name = l.sub(/^[ \t]*Package[ \t]*:[ \t]*/i,"").strip + if not package_name.empty? then + package = Package.new(package_name) + package.source = common_source + package.version = common_version + package.maintainer = common_maintainer + else + raise RuntimeError, "Package name is not set in \"#{file}\" file" + end + when /^\*[ \t]*([0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*)[ \t]*$/ then + state = "CHANGELOG" + change_log[change_version] = change_contents.strip if not change_version.nil? + change_version = $1 + change_contents = "" + when /^Include$/i then + pfile = File.dirname(file) + "/" + l.sub(/^[ \t]*Include[ \t]*:[ \t]*/i,"").strip + if File.exist? pfile then + pkglist = pkglist + (Parser.read_multy_pkginfo_from pfile) + list = Parser.read_multy_pkginfo_from(pfile, true) + common_source = list[0] if common_source.empty? + common_version = list[1] if common_version.empty? + common_maintainer = list[2] if common_maintainer.empty? + change_log = list[3] if change_log.empty? + else + raise RuntimeError, "Not exist \"#{pfile}\"" + end + when /^ORIGIN$/ then #for compatable + multi_line = nil + next + else raise RuntimeError, "Can't parse below line in \"#{file}\" file \n\t#{l}" + end + when "COMMON" then + case field_name + when /^$/ then state = "INIT" + when /^Source$/i then + state = "COMMON" + if common_source.empty? then common_source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^Version$/i then + state = "COMMON" + if common_version.empty? then common_version = check_version l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^Maintainer$/i then + state = "COMMON" + if common_maintainer.empty? then common_maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + when /^ORIGIN$/ then #for compatable + multi_line = nil + next + else raise RuntimeError, "Can't parse below line in \"#{file}\" file \n\t#{l}" + end + when "PACKAGE" then + case field_name + when /^$/ then + state = "INIT" + if not package.package_name.empty? then + pkglist.push package + package = nil + else raise RuntimeError, "Package name is not set in \"#{file}\" file" + end + multi_line = nil + when /^Source$/i then + if common_source.empty? and package.source.empty? then package.source = l.sub(/^[ \t]*Source[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + multi_line = nil + when /^Version$/i then + if common_version.empty? and package.version.empty? then package.version = check_version l.sub(/^[ \t]*Version[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + multi_line = nil + when /^Maintainer$/i then + if common_maintainer.empty? and package.maintainer.empty? then package.maintainer = l.sub(/^[ \t]*Maintainer[ \t]*:[ \t]*/i,"").strip + else raise RuntimeError, "#{field_name} information is conflict in \"#{file}\" file\nIf use #{field_name} field in Common section then Package section can't contain #{field_name} field" + end + multi_line = nil + when /^OS$/i then + package.os_list = l.sub(/^[ \t]*OS[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split(",") + package.os = package.os_list[0] + multi_line = nil + when /^Label$/i then + package.label = l.sub(/^[ \t]*Label[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^Build-host-os$/i then + package.build_host_os = l.sub(/^[ \t]*Build-host-os[ \t]*:[ \t]*/i,"").tr(" \t\n\r", "").split(",") + multi_line = nil + when /^Attribute$/i then + package.attribute = l.sub(/^[ \t]*Attribute[ \t]*:[ \t]*/i,"").tr(" \t\n\r","").split("|") + multi_line = nil + when /^Install-dependency$/i then + package.install_dep_list = dep_parser l.sub(/^[ \t]*Install-dependency[ \t]*:[ \t]*/i,"").split(',') + multi_line = nil + when /^Build-dependency$/i then + package.build_dep_list = dep_parser l.sub(/^[ \t]*Build-dependency[ \t]*:[ \t]*/i,"").split(',') + multi_line = nil + when /^Source-dependency$/i then + package.source_dep_list = dep_parser l.sub(/^[ \t]*Source-dependency[ \t]*:[ \t]*/i,"").split(',') + multi_line = nil + when /^Conflicts$/i then + package.conflicts = dep_parser l.sub(/^[ \t]*Conflicts[ \t]*:[ \t]*/i,"").split(',') + multi_line = nil + when /^Src-path$/i then + package.src_path = l.sub(/^[ \t]*Src-path[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^Path$/i then + package.path = l.sub(/^[ \t]*Path[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^Origin$/i then + package.origin = l.sub(/^[ \t]*Origin[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^SHA256$/i then + package.checksum = l.sub(/^[ \t]*SHA256[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^Size$/i then + package.size = l.sub(/^[ \t]*Size[ \t]*:[ \t]*/i,"").strip + multi_line = nil + when /^Description$/i then + package.description = l.sub(/^[ \t]*Description[ \t]*:[ \t]*/i,"").strip + multi_line = "Description" + when /^ORIGIN$/ then #for compatable + multi_line = nil + next + when /^C-/ then + if package.custom.empty? then package.custom = l.rstrip + else package.custom = package.custom + "\n" + l.rstrip + end + multi_line = nil + else + if multi_line.nil? then raise RuntimeError, "Can't parse below line in \"#{file}\" file \n\t#{l}" end + case multi_line + when "Description" then package.description = package.description + "\n" + l.rstrip + else raise RuntimeError, "Can't parse below line in \"#{file}\" file \n\t#{l}" + end + end + when "CHANGELOG" then + case field_name + when /^$/ then + state = "INIT" + if not change_version.nil? then + if change_log[change_version].nil? then change_log[change_version] = change_contents.strip + else raise RuntimeError, "change log version is duplicated in \"#{file}\" file \n\t#{l}" + end + end + change_version = nil + change_contents = "" + when /^\*[ \t]*([0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*)[ \t]*$/ then + if not change_version.nil? then + if change_log[change_version].nil? then change_log[change_version] = change_contents.strip + else raise RuntimeError, "change log version is duplicated in \"#{file}\" file \n\t#{l}" + end + end + change_version = $1 + change_contents = "" + else + change_contents = change_contents + "\n" + l.rstrip + end + else raise RuntimeError, "UNKNOWN state #{field_name}" + end + end + + # check last package + if not package.nil? then pkglist.push package end + if not change_version.nil? then + if change_log[change_version].nil? then change_log[change_version] = change_contents.strip + else raise RuntimeError, "change log version is duplicated in \"#{file}\" file \n\t#{change_version}" + end + end + pkglist.each {|pkg| pkg.change_log = change_log } + end + if only_common then return [common_source, common_version, common_maintainer, change_log] end + return pkglist + end + + def Parser.check_version(version) + if not version =~ /^[0-9][0-9]*.[0-9][0-9]*.[0-9][0-9]*$/ then + raise RuntimeError, "Version format not matched \"#{version}\"\nVersion format must be \"{digit}.{digit}.{digit}\"" + end + return version + end + + def Parser.read_changelog(file) + return read_multy_pkginfo_from(file,true)[3] + end + + def Parser.read_single_pkginfo_from (file) + return read_multy_pkginfo_from(file)[0] + end + + def Parser.read_repo_pkg_list_from (file) + result = {} + read_multy_pkginfo_from(file).each { |x| result[x.package_name]=x } + return result + end + + #for test + def Parser.print (array) + array.each do |package| + puts package.to_s + puts "" + end + end + + private + def Parser.dep_parser (string_list) + dependency_list = [] + string_list.each do |dep| + #variable initialize + package_name = "" + comp = nil + base_version = nil + target_os_list = [] + #string trim + dependency = dep.tr " \t\r\n", "" + #version extract + vs = dependency.index('(') + ve = dependency.index(')') + if not vs.nil? and not ve.nil? then + comp = dependency[(vs+1)..(vs+2)] + base_version = dependency[(vs+3)..(ve-1)] + end + #os list extract + os = dependency.index('[') + oe = dependency.index(']') + if not os.nil? and not oe.nil? then + target_os_list = dependency[(os+1)..(oe-1)].split("|") + end + # package_name extract + pe = dependency.index(/[\]\[\)\(]/) + if pe.nil? + package_name = dependency + else + package_name = dependency[0..pe-1] + end + #package_name check + if not package_name.empty? then + dependency_list.push Dependency.new(package_name,comp,base_version,target_os_list) + end + end + return dependency_list + end +end diff --git a/src/common/utils.rb b/src/common/utils.rb index 24b12ff..abae33c 100644 --- a/src/common/utils.rb +++ b/src/common/utils.rb @@ -1,6 +1,6 @@ =begin - - utils.rb + + utils.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -25,178 +25,304 @@ limitations under the License. Contributors: - S-Core Co., Ltd =end +require 'rbconfig' class Utils + STARTUP_INFO_SIZE = 68 + PROCESS_INFO_SIZE = 16 + NORMAL_PRIORITY_CLASS = 0x00000020 + + def Utils.identify_current_OS() + os = "UnsupportedOS" - if defined?(HOST_OS).nil? then case `uname -s`.strip - when "Linux" - HOST_OS = "linux" - when /MINGW32.*/ - HOST_OS = "windows" - when "Darwin" - HOST_OS = "darwin" - else - end + when "Linux" + if File.exist? "/etc/debian_version" then + arch = (`uname -i`.strip == "x86_64") ? "64" : "32" + os = "ubuntu-#{arch}" + elsif File.exist? "/etc/redhat-release" then + os = "redhat-unknown" + elsif File.exist? "/etc/SuSE-release" then + arch = (`uname -i`.strip == "x86_64") ? "64" : "32" + os = "opensuse-#{arch}" + elsif File.exist? "/etc/mandrake-release" then + os = "mandrake-unknown" + end + when "MINGW32_NT-5.1" + progfile_path = Utils.execute_shell_return("echo $PROGRAMFILES","windows")[0].strip + if progfile_path.include?("(x86)") then arch = "64" else arch = "32" end + os = "windows-#{arch}" + when "MINGW32_NT-6.1" + progfile_path = Utils.execute_shell_return("echo $PROGRAMFILES","windows")[0].strip + if progfile_path.include?("(x86)") then arch = "64" else arch = "32" end + os = "windows-#{arch}" + when "Darwin" + os = "macos-64" + end + + return os end - # set static variable in WORKING_DIR, HOME - if defined?(WORKING_DIR).nil? then WORKING_DIR = Dir.pwd end - if defined?(HOME).nil? then - # get home directory, using Dir.chdir - Dir.chdir - HOME = Dir.pwd - Dir.chdir WORKING_DIR - end - - def Utils.create_uniq_name - time = Time.new - # uniq snapshot_name name is year_month_day_hour_min_sec_microsec - return time.strftime("%m%d%H%M%S") + time.usec.to_s() - end - - def Utils.is_url_remote(url) - if url.nil? then - return false - end - - protocol = url.split(':')[0] - - case protocol - when "http" then - return true - else - return false - end - end - - # if source_ver > target_ver, return -1 - # if source_ver < target_ver, return 1 - # if source_ver == target_ver, return 0 - def Utils.compare_version(source_ver, target_ver) - sver = source_ver.split('-')[0] - tver = target_ver.split('-')[0] - - arr_sver = sver.split('.') - arr_tver = tver.split('.') - - slen = arr_sver.length - tlen = arr_tver.length - len = tlen - - if slen > tlen then - gap = slen - tlen - gap.times do - arr_tver.push("0") - end - len = slen - elsif tlen > slen then - gap = tlen - slen - gap.times do - arr_sver.push("0") - end - len = tlen - end - - len.times do |i| - if arr_sver[i].to_i < arr_tver[i].to_i then - return 1 - elsif arr_sver[i].to_i > arr_tver[i].to_i then - return -1 - end - end - - return 0 - end - - - def Utils.execute_shell(cmd) - ret = false - if HOST_OS.eql? "windows" then + + def Utils.check_host_OS() + if Utils.get_all_OSs().include? HOST_OS then + return true + else + return false + end + end + + + def Utils.get_all_OSs() + return ["ubuntu-32","ubuntu-64","windows-32","windows-64","macos-64","opensuse-32", "opensuse-64"] + end + + + def Utils.create_uniq_name + time = Time.new + + # uniq snapshot_name name is year_month_day_hour_min_sec_microsec + return time.strftime("%m%d%H%M%S") + time.usec.to_s.rjust(6, '0') + end + + def Utils.is_url_remote(url) + if url.nil? then + return false + end + + protocol = url.split(':')[0] + + case protocol + when "http" then + return true + else + return false + end + end + + # if source_ver > target_ver, return -1 + # if source_ver < target_ver, return 1 + # if source_ver == target_ver, return 0 + def Utils.compare_version(source_ver, target_ver) + sver = source_ver.split('-')[0] + tver = target_ver.split('-')[0] + + arr_sver = sver.split('.') + arr_tver = tver.split('.') + + slen = arr_sver.length + tlen = arr_tver.length + len = tlen + + if slen > tlen then + gap = slen - tlen + gap.times do + arr_tver.push("0") + end + len = slen + elsif tlen > slen then + gap = tlen - slen + gap.times do + arr_sver.push("0") + end + len = tlen + end + + len.times do |i| + if arr_sver[i].to_i < arr_tver[i].to_i then + return 1 + elsif arr_sver[i].to_i > arr_tver[i].to_i then + return -1 + end + end + + return 0 + end + + + def Utils.generate_shell_command(cmd, os_category = nil) + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then mingw_path = "sh.exe -c " cmd = cmd.gsub("\"", "\\\"") - cmd = mingw_path + "\"#{cmd}\"" + cmd = mingw_path + "\"#{cmd}\"" end - system "#{cmd}" - if $?.to_i == 0 then ret = true else ret = false end - + return cmd + end + + + def Utils.execute_shell(cmd, os_category = nil) + ret = false + + # generate command + cmd = generate_shell_command(cmd, os_category) + + `#{cmd}` + if $?.to_i == 0 then ret = true else ret = false end + return ret end - def Utils.execute_shell_return(cmd) + def Utils.execute_shell_return(cmd, os_category = nil) result_lines = [] - if HOST_OS.eql? "windows" then - mingw_path = "sh.exe -c " - cmd = cmd.gsub("\"", "\\\"") - cmd = mingw_path + "\"#{cmd}\"" - end + # generate command + cmd = generate_shell_command(cmd, os_category) # get result - IO.popen("#{cmd} 2>&1") { |io| + IO.popen("#{cmd} 2>&1") do |io| io.each do |line| result_lines.push line end - } - - if $?.to_i == 0 then + end + + if $?.to_i == 0 then return result_lines - else + else return nil - end + end end - def Utils.execute_shell_return_ret(cmd) - if HOST_OS.eql? "windows" then - mingw_path = "sh.exe -c " - cmd = cmd.gsub("\"", "\\\"") - cmd = mingw_path + "\"#{cmd}\"" - end + def Utils.execute_shell_return_ret(cmd, os_category = nil) - return `#{cmd}` - end + # generate command + cmd = generate_shell_command(cmd, os_category) - def Utils.execute_shell_with_log(cmd, log) + return `#{cmd}` + end - if HOST_OS.eql? "windows" then - mingw_path = "sh.exe -c " + + # create process and log its all output(stderr, stdout) + # can decide whether wait or not + def Utils.execute_shell_with_log(cmd, log_path, wait=true) + + if log_path.nil? or log_path.empty? then + return execute_shell_with_stdout(cmd, nil) + end + + ruby_path=File.join(Config::CONFIG["bindir"], + Config::CONFIG["RUBY_INSTALL_NAME"] + + Config::CONFIG["EXEEXT"]) + + # call execute + os_category = get_os_category( HOST_OS ) + if os_category == "windows" then cmd = cmd.gsub("\"", "\\\"") - cmd = mingw_path + "\"#{cmd}\"" end + cmd = "#{ruby_path} \"#{File.expand_path(File.dirname(__FILE__))}/execute_with_log.rb\" \"#{cmd}\" \"#{log_path}\"" + + # print log + pipe = IO.popen("#{cmd} 2>&1") + if wait then + return Process.waitpid2(pipe.pid) + else + return [pipe.pid,nil] + end + end + + + def Utils.execute_shell_with_stdout(cmd, os_category = nil) + + logger = Logger.new(STDOUT) + + # generate command + cmd = generate_shell_command(cmd, os_category) + # print log - IO.popen("#{cmd} 2>&1") { |io| + pipe = IO.popen("#{cmd} 2>&1") do |io| io.each do |line| - log.output line + logger.info line end - } - - if $?.to_i == 0 then - return true - else - return false - end + end + + return [nil, nil] end + def Utils.spawn(cmd, os_category = nil) + + if os_category.nil? then os_category = get_os_category( HOST_OS ) end + + if os_category == "windows" then + create_process(cmd) + else + fork do + exec(cmd) + end + end + + end + + def Utils.create_process(command,redirStdout="", redirStderr="") + + if redirStdout.length > 0 + tmpfile = File.new(redirStdout,"w") + save_stdout = $stdout.clone + $stdout.reopen(tmpfile) + end + + if redirStderr.length > 0 + tmpfile = File.new(redirStderr,"w") + save_stderr = $stderr.clone + $stderr.reopen(tmpfile) + end + + params = [ + 'L', # IN LPCSTR lpApplicationName + 'P', # IN LPSTR lpCommandLine + 'L', # IN LPSECURITY_ATTRIBUTES lpProcessAttributes + 'L', # IN LPSECURITY_ATTRIBUTES lpThreadAttributes + 'L', # IN BOOL bInheritHandles + 'L', # IN DWORD dwCreationFlags + 'L', # IN LPVOID lpEnvironment + 'L', # IN LPCSTR lpCurrentDirectory + 'P', # IN LPSTARTUPINFOA lpStartupInfo + 'P' # OUT LPPROCESS_INFORMATION lpProcessInformation + ] + returnValue = 'I' # BOOL + + startupInfo = [STARTUP_INFO_SIZE].pack('I') + ([0].pack('I') * (STARTUP_INFO_SIZE - 4)) + processInfo = [0].pack('I') * PROCESS_INFO_SIZE + + createProcess = Win32API.new("kernel32", "CreateProcess", params, returnValue) + + createProcess.call(0, command, 0, 0, 0, NORMAL_PRIORITY_CLASS, 0, 0, startupInfo, processInfo) + + if redirStdout.length > 0 + $stdout.reopen(save_stdout) + end + + save_stdout.close if save_stdout + + if redirStderr.length > 0 + $stderr.reopen(save_stderr) + end + + save_stderr.close if save_stderr + + ($0 == __FILE__ ) ? processInfo : processInfo.unpack("LLLL")[2] + end + def Utils.is_absolute_path(path) - if HOST_OS.eql? "linux" or HOST_OS.eql? "darwin" then + if is_unix_like_os( HOST_OS ) then # if path start "/" then absoulte path if path.start_with?("/") then return true else - return false + return false end - elsif HOST_OS.eql? "windows" then + elsif is_windows_like_os( HOST_OS ) then # if path start "c:/" or "D:/" or ... then absoulte path if path =~ /^[a-zA-Z]:[\/]/ then return true else return false end - else + else puts "HOST_OS is invalid" end end @@ -204,17 +330,313 @@ class Utils # this will be used on MinGW/MSYS def Utils.get_unix_path(path) - if HOST_OS.eql? "linux" or HOST_OS.eql? "darwin" then + if is_unix_like_os( HOST_OS ) then return path - elsif HOST_OS.eql? "windows" then + elsif is_windows_like_os( HOST_OS ) then new_path = path if is_absolute_path( new_path ) then new_path = "/" + new_path[0,1] + new_path[2..-1] end return new_path - else + else puts "HOST_OS is invalid" return path end end + + def Utils.file_lock(lock_file_name) + lock_file = File.new(lock_file_name, File::RDWR|File::CREAT, 0644) + lock_file.flock(File::LOCK_EX) + lock_file.rewind + lock_file.flush + lock_file.truncate(lock_file.pos) + + return lock_file + end + + def Utils.file_unlock(lock_file) + lock_file.close + end + + def Utils.parse_server_addr(saddr) + addr = saddr.split(":") + return nil unless addr.length == 2 + return addr + end + + def Utils.parse_ftpserver_url(surl) + return nil unless surl.start_with? "ftp://" + + surl = surl[6..-1] + parse1 = surl.split("@") + return nil unless parse1.length == 2 + + idpw = parse1[0] + url = parse1[1] + parse1 = idpw.split(":") + return nil unless parse1.length == 2 + + id = parse1[0] + passwd = parse1[1] + if url.end_with? "/" then url = url.chop end + + parse1 = url.split(":") + if parse1.length == 2 then + ip = parse1[0] + port = parse1[1] + elsif parse1.length == 1 then + ip = parse1[0] + port = 21 + else + return nil + end + + return [ip, port, id, passwd] + end + + + def Utils.generate_ftp_url(addr, port, username, passwd) + return "ftp://#{username}:#{passwd}@#{addr}:#{port}" + end + + + def Utils.extract_a_file(file_path, target_file, path) + dirname = File.dirname(file_path) + filename = File.basename(file_path) + ext = File.extname(filename) + + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(file_path) + _path = Utils.get_unix_path(path) + + case ext + when ".zip" then + if not path.nil? then + extract_file_command = "unzip -xo #{_package_file_path} #{target_file} -d #{_path}" + else + extract_file_command = "unzip -xo #{_package_file_path} #{target_file}" + end + when ".tar" then + if not path.nil? then + extract_file_command = "tar xf #{_package_file_path} -C #{_path} #{target_file}" + else + extract_file_command = "tar xf #{_package_file_path} #{target_file}" + end + end + + # check exit code + ret = execute_shell "#{extract_file_command}" + if not ret then return false end + + # check result file + if not path.nil? then + target_file_path = File.join(path, target_file) + else + target_file_path = target_file + end + + if not File.exist? target_file_path then + return false + else + return true + end + end + + + # check if the os is windows-like + def Utils.is_windows_like_os(os_name) + if os_name.start_with? "windows-" then + return true + else + return false + end + end + + + # check if the os is unix-like + def Utils.is_unix_like_os(os_name) + if os_name.start_with? "ubuntu-" or + os_name.start_with? "opensuse-" or + os_name.start_with?"macos-" then + return true + else + return false + end + end + + + # check if the os is linux-like + def Utils.is_linux_like_os(os_name) + if os_name.start_with? "ubuntu-" or + os_name.start_with? "opensuse-" then + return true + else + return false + end + end + + + # check if the os is macos-like + def Utils.is_macos_like_os(os_name) + if os_name.start_with?"macos-" then + return true + else + return false + end + end + + + def Utils.get_os_category(os_name) + if os_name.start_with? "ubuntu-" or os_name.start_with? "opensuse-" then + return "linux" + elsif os_name.start_with?"macos-" then + return "macos" + elsif os_name.start_with? "windows-" then + return "windows" + else + return os_name + end + end + + + def Utils.get_package_name_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[0] + end + return nil + end + + + def Utils.get_version_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[1] + end + return nil + end + + + def Utils.get_os_from_package_file( local_path ) + filename = File.basename(local_path) + if filename =~ /.*_.*_.*\.zip/ then + new_name = filename.sub(/(.*)_(.*)_(.*)\.zip/,'\1,\2,\3') + return new_name.split(",")[2] + end + return nil + end + + def Utils.multi_argument_test( arg, seperator ) + return ( not arg.end_with? seperator and not arg.split( seperator ).select{|x| x.empty?}.length > 0 ) + end + + def Utils.directory_emtpy?(dir_path) + return (Dir.entries(dir_path).join == "...") + end + + + def Utils.checksum(file_path) + if File.exist? file_path then + return Utils.execute_shell_return("shasum -a 256 \"#{file_path}\"")[0].split(" ")[0] + else + return nil + end + end + + def Utils.get_version() + version_file = "#{File.dirname(__FILE__)}/../../VERSION" + + if not File.exist? version_file then + return nil + end + + f = File.open( version_file, "r" ) + version = f.readline + f.close + + return version + end + + + def Utils.kill_process(base_pid) + # stop process execution + # NOTE. On MinGW "STOP" option stop whole process, so don't use it + os_category = get_os_category(HOST_OS) + if os_category != "windows" then + Process.kill("STOP", base_pid) + end + + # get all sub processes # kill&wait + sub_pids = get_sub_processes(base_pid) + sub_pids.each do |pid| + Utils.kill_process(pid) + end + + begin + Process.kill(9, base_pid) + Process.waitpid2(base_pid) + rescue + # do nothing + end + end + + + def Utils.get_sub_processes(base) + result = [] + + # generate pid => ppid hash + # NOTE. MinGW does not support "-o" option and has different output format + os_category = get_os_category(HOST_OS) + if os_category != "windows" then + Hash[*`ps -eo pid,ppid`.scan(/\d+/).map{|x| x.to_i}].each do |pid,ppid| + if ppid == base then + result.push pid + end + end + else + # NOTE.On windows, there two types of pid. win pid, mingw pid + # Sometime these different pids has same value with difference process + # Root of pid is win pid and leaf node is mingw pid + # So, after we get mingw pid, win pid should not be checked. + # gather MinGW/MSYS process id + Hash[*`ps -e`.scan(/^[\s]*(\d+)[\s]+(\d+)/).flatten.map{|x| x.to_i}].each do |pid,ppid| + if ppid == base then + result.push pid + end + end + + # gather windows process id if no mingw pid + if result.count == 0 then + require 'rubygems' + require 'sys/proctable' + Sys::ProcTable.ps do |proc| + if proc.ppid == base then + result.push proc.pid + end + end + end + end + + result.uniq! + return result + end + + + + if defined?(HOST_OS).nil? then + HOST_OS = Utils.identify_current_OS() + end + + # set static variable in WORKING_DIR, HOME + if defined?(WORKING_DIR).nil? then WORKING_DIR = Dir.pwd end + if defined?(HOME).nil? then + # get home directory, using Dir.chdir + Dir.chdir + HOME = Dir.pwd + Dir.chdir WORKING_DIR + end + + end diff --git a/src/pkg_server/DistSync.rb b/src/pkg_server/DistSync.rb new file mode 100644 index 0000000..a79ea4b --- /dev/null +++ b/src/pkg_server/DistSync.rb @@ -0,0 +1,97 @@ +=begin + + DistSync.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require "fileutils" +require "thread" +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "Action.rb" +require "ScheduledActionHandler.rb" + +class DistSyncAction < Action + + def initialize(time, pkgserver, dist_name ) + super(time, pkgserver.sync_interval) + + @pkgserver = pkgserver + @dist_name = dist_name + end + + + def init + end + + + def execute + # Start to sync job + @pkgserver.log.info "Executing sync action for the #{@dist_name}" + begin + execute_internal() + rescue => e + @pkgserver.log.error e.message + @pkgserver.log.error e.backtrace.inspect + end + end + + + private + def execute_internal() + # update pkg info + @pkgserver.reload_dist_package + + # sync + @pkgserver.sync( @dist_name, false ) + end +end + + +class DistSync + attr_accessor :quit + + # init + def initialize( server ) + @server = server + @handler = ScheduledActionHandler.new + end + + # start thread + def start() + # scan all sync distribution + @server.distribution_list.each do |dist| + # if dist does not have parent server then skip sync + if dist.server_url.empty? then next end + + time = Time.now + @server.log.info "Registered sync-action for dist : #{dist.name}" + @handler.register(DistSyncAction.new(time, @server, dist.name)) + end + + # start handler + @handler.start + end +end diff --git a/src/pkg_server/SocketRegisterListener.rb b/src/pkg_server/SocketRegisterListener.rb new file mode 100644 index 0000000..9982274 --- /dev/null +++ b/src/pkg_server/SocketRegisterListener.rb @@ -0,0 +1,240 @@ +=begin + + SocketRegisterListener.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'socket' +require 'thread' +$LOAD_PATH.unshift File.dirname(__FILE__) +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" +require "packageServerConfig" +require "BuildComm" +require "net/ftp" + +# mutax for register operation +$register_mutex = Mutex.new + +class SocketRegisterListener + + # initialize + def initialize (parent) + @parent_server = parent + @thread = nil + @finish_loop = false + @log = @parent_server.log + end + + # start listening + def start() + @log.info "SocketRegisterListener start" + @thread = Thread.new do + main() + end + end + + # quit listening + def stop_listening() + @finish_loop = true + end + + private + + # thread main + def main() + @log.info "SocketRegisterListener entering main loop" + # server open + begin + @comm_server = BuildCommServer.create(@parent_server.port, @log) + rescue => e + @log.info "Server creation failed" + @log.error e.message + @log.error e.backtrace.inspect + return + end + + # loop + @log.info "Entering Control Listening Loop ... " + @finish_loop = false + @comm_server.wait_for_connection(@finish_loop) do |req| + begin + handle_job_request( req ) + rescue => e + @log.info "error occured in handle_job_request function" + @log.error e.message + @log.error e.backtrace.inspect + end + end + + # quit + @comm_server.terminate + end + + # wait for job requests + def wait_for_job_requests + req_list = [] + req_list.push @tcp_server.accept + + return req_list + end + + # handle job request + def handle_job_request( req ) + + # read request + req_line = req.gets + if req_line.nil? then return end + + # parse request + cmd = "" + if req_line.split("|").count > 0 then + cmd = req_line.split("|")[0].strip + end + + case cmd + when "UPLOAD" + Thread.new do + handle_cmd_upload( req_line, req ) + end + when "REGISTER" + Thread.new do + handle_cmd_register( req_line, req ) + end + when "STOP" + handle_cmd_stop( req_line, req ) + else + @log.error "Received Unknown REQ: #{req_line}" + end + @log.info "REQ processing done" + end + + # "UPLOAD" + def handle_cmd_upload( line, req ) + @log.info "Received File transfer REQ : #{line}" + + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count > 1 then + dock_name = tok[1].strip + incoming_dir = "#{@parent_server.incoming_path}/#{dock_name}" + FileUtils.mkdir_p(incoming_dir) + else + incoming_dir = "#{@parent_server.incoming_path}" + end + + file_path_list = [] + begin + @comm_server.receive_file(req, incoming_dir) + rescue => e + @log.error "Failed to transfer file" + @log.error e.message + @log.error e.backtrace.inspect + end + BuildCommServer.send_end(req) + end + + # "Register" + def handle_cmd_register( line, req ) + @log.info "Received register REQ : #{line}" + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count < 3 then + @log.error "Received Wrong REQ : #{line}" + BuildCommServer.send(req, "ERROR|Invalid REQ format") + return + end + dist_name = tok[1].strip + + if tok[2].start_with? "DOCK" then + dock_name = tok[3] + idx = 4 + else + dock_name = "" + idx = 2 + end + + file_path_list = [] + + while idx < tok.length do + if dock_name.empty? then + file_path_list.push "#{@parent_server.incoming_path}/#{tok[idx]}" + else + file_path_list.push "#{@parent_server.incoming_path}/#{dock_name}/#{tok[idx]}" + end + idx = idx + 1 + end + # register mutex + $register_mutex.synchronize do + begin + @parent_server.reload_dist_package() + snapshot_name = @parent_server.register( file_path_list, dist_name, true, false, true) + BuildCommServer.send(req,"SUCC|#{snapshot_name}") + rescue => e + @log.error "register failed" + @log.error e.message + @log.error e.backtrace.inspect + BuildCommServer.send(req, "ERROR|#{e.message}") + @parent_server.release_lock_file + return + end + end + + if not dock_name.empty? then + FileUtils.rm_rf "#{@parent_server.incoming_path}/#{dock_name}" + end + + BuildCommServer.send_end(req) + end + + # "STOP" + def handle_cmd_stop( line, req ) + @log.info "Received STOP REQ" + + BuildCommServer.send_begin(req) + + tok = line.split("|").map { |x| x.strip } + if tok.count < 2 then + @log.error "Received Wrong REQ : #{line}" + BuildCommServer.send(req, "ERROR|Invalid REQ format") + return + end + passwd = tok[1].strip + + if @parent_server.passwd.eql? passwd then + @parent_server.finish = true + @log.info "Package server stop flag set" + BuildCommServer.send(req,"SUCC") + else + @log.info "Received stop command, but passwd mismatched : #{passwd}" + BuildCommServer.send(req,"ERROR|Password mismatched!") + end + BuildCommServer.send_end(req) + BuildCommServer.disconnect(req) + end +end + diff --git a/src/pkg_server/client.rb b/src/pkg_server/client.rb index 8bffbd2..a3e3815 100644 --- a/src/pkg_server/client.rb +++ b/src/pkg_server/client.rb @@ -1,7 +1,6 @@ - =begin - - client.rb + + client.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,1445 +27,1714 @@ Contributors: =end require "fileutils" +require "thread" +require "net/ftp" $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" require "downloader" require "installer" -require "serverConfig" +require "packageServerConfig" require "package" require "parser" require "utils" require "log" require "Version" +require "BuildComm" +require "FileTransferViaFTP" +require "FileTransferViaDirect" + +$update_mutex = Mutex.new +$get_snapshot_mutex = Mutex.new +$filemove_mutex = Mutex.new class Client - # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] - PKG_LIST_FILE_PREFIX = "pkg_list_" - INSTALLED_PKG_LIST_FILE = "installedpackage.list" - CONFIG_PATH = "#{$build_tools}/client" - PACKAGE_INFO_DIR = ".info" - DEFAULT_INSTALL_DIR = "#{Utils::HOME}/build_root" - DEFAULT_SERVER_ADDR = "http://172.21.17.55/dibs/unstable" - - attr_accessor :server_addr, :location, :pkg_hash_os, :is_server_remote, :installed_pkg_hash_loc, :archive_pkg_list, :all_dep_list, :log - - public - # initialize - # create "remote package hash" and "installed package hash" - # @server_addr = server address (can be included distribution, snapshot) - # @location = client location (download and install file to this location) - def initialize(server_addr, location, logger) - - # create directory + # constant + PKG_LIST_FILE_PREFIX = "pkg_list_" + INSTALLED_PKG_LIST_FILE = "installedpackage.list" + CONFIG_PATH = "#{PackageServerConfig::CONFIG_ROOT}/client" + PACKAGE_INFO_DIR = ".info" + DEFAULT_INSTALL_DIR = "#{Utils::HOME}/build_root" + DEFAULT_SERVER_ADDR = "http://172.21.17.55/dibs/unstable" + OS_INFO_FILE = "os_info" + ARCHIVE_PKG_LIST_FILE = "archive_pkg_list" + + attr_accessor :server_addr, :location, :pkg_hash_os, :is_server_remote, :installed_pkg_hash_loc, :archive_pkg_list, :all_dep_list, :log, :support_os_list, :config_dist_path, :download_path, :tmp_path, :snapshot_path, :snapshots_path, :snapshot_url + + public + # initialize + # create "remote package hash" and "installed package hash" + # @server_addr = server address (can be included distribution, snapshot) + # @location = client location (download and install file to this location) + def initialize(server_addr, location, logger) + + # set log + if logger.nil? or logger.class.to_s.eql? "String" then + @log = DummyLog.new() + else + @log = logger + end + + # create directory if not File.exist? CONFIG_PATH then FileUtils.mkdir_p "#{CONFIG_PATH}" end - # set default server address, location - if server_addr.nil? then server_addr = get_default_server_addr() end - if location.nil? then location = get_default_inst_dir() end - - # chop server address, if end with "/" - if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end - - @server_addr = server_addr - @location = location - @pkg_hash_os = {} - @installed_pkg_hash_loc = {} - @archive_pkg_list = [] - @all_dep_list = [] - @is_server_remote = Utils.is_url_remote(server_addr) - - # set log - if logger.nil? or logger.class.to_s.eql? "String" then - @log = Log.new(logger) - else - @log = logger - end - - FileInstaller.set_logger(@log) - FileDownLoader.set_logger(@log) - - # read installed pkg list, and create hash - FileUtils.mkdir_p "#{@location}" - create_installed_pkg_hash() - - # readk remote pkg list, and hash list - create_remote_pkg_hash(false) - @log.info "Initialize - #{server_addr}, #{location}" - end - - public - # update package list from server - def update() - if not create_remote_pkg_hash(true) then - @log.error "\"#{@server_addr}\" does not have package list file properly." - return false - end - create_default_config(@server_addr) - @log.info "Update package list from \"#{@server_addr}\".. OK" - return true - end - - public - # download package - def download(pkg_name, os, trace) - - dependent_pkg_list = [] - - # get dependent list - if trace then - dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, false) - if dependent_pkg_list.nil? then - @log.error "Failed to get dependency for \"#{pkg_name}\" package" - return false - end - else dependent_pkg_list = [pkg_name] end - - surl = nil - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - surl = @server_addr + "/../.." - else - surl = @server_addr - end - - # download files - file_local_path = [] - dependent_pkg_list.each do |p| - pkg_path = get_attr_from_pkg(p, os, "path") - pkg_ver = get_attr_from_pkg(p, os, "version") - url = surl + pkg_path - filename = pkg_path.split('/')[-1] - if not FileDownLoader.download(url, @location) then - @log.error "Failed download #{pkg_name} [#{pkg_ver}]" - return nil - end - - file_local_path.push(File.join(@location, filename)) - @log.info "Downloaded \"#{pkg_name} [#{pkg_ver}]\" package file.. OK" - @log.info " [path : #{file_local_path.join(", ")}]" - end - - return file_local_path - end - - public - # download source package - def download_source(pkg_name, os) - - # get source file path - src_path = get_attr_from_pkg(pkg_name, os, "src_path") - if src_path.nil? or src_path.empty? then - @log.error "#{pkg_name} package does not have source" - return nil - end - file_url = nil - - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - surl = @server_addr + "/../.." + src_path - else - surl = @server_addr + src_path - end - - # download file - filename = src_path.split('/')[-1] - if not FileDownLoader.download(surl, @location) then - @log.error "Failed download #{pkg_name} source" - return nil - end - file_local_path = File.join(@location, filename) - @log.info "Downloaded source of #{pkg_name} package.. OK" - @log.info " [path : #{file_local_path}]" - - return file_local_path - end - - public - # download dependent source - def download_dep_source(file_name) - - file_url = nil - - addr_arr = @server_addr.split('/') - if addr_arr[-2].eql? "snapshots" then - file_url = @server_addr + "/../../source/" + file_name - else - file_url = @server_addr + "/source/#{file_name}" - end - if not FileDownLoader.download(file_url, @location) then - @log.error "Failed download #{file_name}" - return nil - end - file_local_path = File.join(@location, file_name) - @log.info "Downloaded \"#{file_name}\" source file.. OK" - @log.info " [path : #{file_local_path}]" - - return file_local_path - end - - public - # check archive file - def check_archive_file(file_name) - - result = false - filename = "archive_pkg_list" - local_file_path = File.join(CONFIG_PATH, filename) - if File.exist? local_file_path then - File.open(local_file_path, "r") do |f| - f.each_line do |l| - if l.strip.eql? file_name.strip then - result = true - break - end - end - end - end - return result - end - - public - # upload package - def upload(ssh_alias, id, binary_path_list, source_path_list, verify) - - # check source path list - if source_path_list.nil? or source_path_list.empty? then - @log.error "source package path should be set." - return nil - end - - # verify ssh alias - verify = false - hostfound = false - sshconfig = "#{Utils::HOME}/.ssh/config" - File.open(sshconfig, "r") do |f| - f.each_line do |l| - if l.strip.upcase.start_with? "HOST" then - al = l.strip.split(' ')[1].strip - if al.eql? ssh_alias then hostfound = true - else next end - end - end - end - - if not hostfound then - @log.error "\"#{ssh_alias}\" does not exist in \".ssh/config\" file" - return nil - end - - # get distribution from server addr - dist = get_distribution() - if dist.nil? then - @log.error "Distribution is nil" - return nil - end - - serveraddr = @server_addr - snapshot = nil - if serveraddr.include? "snapshots" then snapshot = serveraddr.split("/")[-1] end - - # set server homd directory - server_home = `ssh #{ssh_alias} pwd` - server_home = server_home.strip - - # set "pkg-svr" file path - # if pkg-svr exist in path then using pkg-svr - result = `ssh #{ssh_alias} which pkg-svr` - if not( result.nil? or result.empty? or result.strip.empty? ) then - pkg_svr = "pkg-svr" + # set default server address, location + if server_addr.nil? then server_addr = get_default_server_addr() end + if location.nil? then location = get_default_inst_dir() end + + # chop server address, if end with "/" + if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end + + @snapshot_path = nil + @snapshot_url = false + + if is_snapshot_url(server_addr) then + @snapshot_url = true + @server_addr, @snapshot_path = split_addr_and_snapshot(server_addr) + else + @server_addr = server_addr + end + + @location = location + @pkg_hash_os = {} + @installed_pkg_hash_loc = {} + @archive_pkg_list = [] + @all_dep_list = [] + @is_server_remote = Utils.is_url_remote(server_addr) + @support_os_list = [] + @config_dist_path = CONFIG_PATH + "/" + get_flat_serveraddr + @download_path = @config_dist_path + "/downloads" + @tmp_path = @config_dist_path + "/tmp" + @snapshots_path = @config_dist_path + "/snapshots" + + # create directory + if not File.exist? @config_dist_path then FileUtils.mkdir_p "#{@config_dist_path}" end + if not File.exist? @download_path then FileUtils.mkdir_p "#{@download_path}" end + if not File.exist? @snapshots_path then FileUtils.mkdir_p "#{@snapshots_path}" end + if not File.exist? @tmp_path then FileUtils.mkdir_p "#{@tmp_path}" end + + # read installed pkg list, and create hash + if not File.exist? @location then FileUtils.mkdir_p "#{@location}" end + @log.info "Update local package list.. [#{@location}]" + read_installed_pkg_list() + + # read remote pkg list, and hash list + @log.info "Update remote package list and supported os list.." + update() + @log.info "Initialize - #{server_addr}, #{location}" + end + + public + # update package list from server + def update() + if not @snapshot_url then + $get_snapshot_mutex.synchronize do + @snapshot_path = get_lastest_snapshot(@is_server_remote) + end + end + @log.info "The lastest snapshot : #{@snapshot_path}" + if @snapshot_path.nil? then + @log.warn "Failed to get the lastest package list" + @snapshot_path = "" + end + + exists_snapshot = false + if is_snapshot_exist(@snapshot_path) then + @log.info "Snapshot information is already cached [#{get_pkglist_path()}]" + exists_snapshot = true + else + @log.info "Snapshot information is not cached" + end + + list_path = get_pkglist_path() + if list_path.nil? then + @log.error "Failed to get package list path" + return false + end + + clean_list() + + if exists_snapshot then + read_supported_os_list(list_path) + read_remote_pkg_list(list_path) + read_archive_pkg_list(list_path) + else + $update_mutex.synchronize do + uniq_name = Utils.create_uniq_name + tmp_dir = File.join(@config_dist_path, uniq_name) + FileUtils.mkdir_p tmp_dir + if not download_os_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have supported os list file properly." + Utils.execute_shell("rm -rf #{tmp_dir}") + return false + else read_supported_os_list(tmp_dir) end + + if not download_pkg_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have package list file properly." + Utils.execute_shell("rm -rf #{tmp_dir}") + return false + else read_remote_pkg_list(tmp_dir) end + + if not download_archive_pkg_list(@is_server_remote, tmp_dir) then + @log.error "\"#{@server_addr}\" does not have archive package list file properly. This error can be ignored" + else read_archive_pkg_list(tmp_dir) end + Utils.execute_shell("mv #{tmp_dir} #{list_path}") + @log.info "Moved \"#{tmp_dir}\" to" + @log.info " \"#{list_path}\"" + # tmp_dir should be removed whether mv command is failed + Utils.execute_shell("rm -rf #{tmp_dir}") + remove_snapshots() + end + end + + $update_mutex.synchronize do + create_default_config(@server_addr) + @log.info "Update package list from \"#{@server_addr}\".. OK" + end + + return true + end + + private + def clean_list() + @pkg_hash_os.clear + @archive_pkg_list.clear + @support_os_list.clear + @log.info "Cleard package list, supported os list.. OK" + end + + public + # download package + def download(pkg_name, os, trace, loc = nil) + + if loc.nil? then loc = @location end + + if not File.exist? loc then FileUtils.mkdir_p "#{loc}" end + + dependent_pkg_list = [] + + # get dependent list + if trace then + dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, true) + if dependent_pkg_list.nil? then + @log.error "Failed to get dependency for \"#{pkg_name}\" package" + return nil + end + else dependent_pkg_list = [pkg_name] end + + surl = @server_addr + # download files + file_local_path = [] + dependent_pkg_list.each do |p| + pkg_name = get_attr_from_pkg(p, os, "name") + pkg_path = get_attr_from_pkg(p, os, "path") + pkg_ver = get_attr_from_pkg(p, os, "version") + pkg_checksum = get_attr_from_pkg(p, os, "checksum") + pkg_size = get_attr_from_pkg(p, os, "size") + if pkg_path.nil? or pkg_ver.nil? then + @log.error "\"#{p}\" package does not exist in package server. If it exist in package server, then try \"pkg-cli update\"" + return nil + end + url = surl + pkg_path + filename = pkg_path.split('/')[-1] + + if not FileDownLoader.download(url, loc, @log) then + @log.error "File Download Failed!!" + @log.error "* #{url} -> #{loc}" + return nil + end + + file_path = File.join(loc, filename) + file_local_path.push(file_path) + end + + if trace then + @log.info "Downloaded \"#{pkg_name}\" package with all dependent packages.. OK" + end + @log.info " [path: #{file_local_path.join(", ")}]" + + return file_local_path + end + + private + def remove_downloaded_pkgs(pkg_name, os) + pkg_file_prefix = "#{@download_path}/#{pkg_name}_*_#{os}.zip" + pkg_files = Dir[pkg_file_prefix].sort_by { |f| File.mtime(f) }.reverse + + if not pkg_files.nil? and pkg_files.length >= 4 then + Utils.execute_shell("rm -rf #{pkg_files[3..-1].join(" ")}") + @log.info "Removed old package files.." + @log.info " * #{pkg_files[3..-1].join(", ")}" + end + end + + private + def move_downloaded_pkg(filepath, distpath) + if filepath.nil? or filepath == "" then return nil end + filename = filepath.split('/')[-1] + if not File.exist? distpath then FileUtils.mkdir_p "#{distpath}" end + distfile = File.join(distpath, filename) + @log.info "Moving \"#{filename}\" to download cache directory" + @log.info " [path: #{distpath}]" + $filemove_mutex.synchronize do + if not File.exist? distfile then + Utils.execute_shell("mv #{filepath} #{distfile}") + else + Utils.execute_shell("rm -f #{filepath}") + return distfile + end + end + + if File.exist? distfile then return distfile else - # if pkg-svr not exist in path then try ~/tizen_sdk/dev_tools/pkg-svr - result = `ssh #{ssh_alias} which #{server_home}/tizen_sdk/dev_tools/pkg-svr` - if not( result.nil? or result.empty? or result.strip.empty? ) then - pkg_svr = "#{server_home}/tizen_sdk/dev_tools/pkg-svr" - else - @log.error "Can't find server's pkg-svr command" - return nil + @log.info "Failed to move [#{filenamae}] to " + @log.info " [#{distpath}]" + return nil + end + end + + private + def remove_snapshots() + listing_prefix = "#{@snapshots_path}/*" + dirs = Dir[listing_prefix].sort_by { |f| File.mtime(f) }.reverse + + if not dirs.nil? and dirs.length >= 20 then + Utils.execute_shell("rm -rf #{dirs[19..-1].join(" ")}") + @log.info "Removed old snapshots.." + @log.info " * #{dirs[19]} ~ " + end + end + + private + def get_cached_filepath(pkg_filename, pkg_checksum, pkg_size) + + cached_filepath = "#{@download_path}/#{pkg_filename}" + if File.exist? cached_filepath then + checksum = `sha256sum #{cached_filepath}`.split(" ")[0] + size = `du -b #{cached_filepath}`.split[0].strip + if checksum.eql? pkg_checksum and size.eql? pkg_size then + return cached_filepath + end + end + return nil + end + + public + # download dependent source + def download_dep_source(file_name) + + file_url = @server_addr + "/source/#{file_name}" + if not FileDownLoader.download(file_url, @location, @log) then + @log.error "Failed download #{file_name}" + return nil + end + file_local_path = File.join(@location, file_name) + @log.info "Downloaded \"#{file_name}\" source file.. OK" + @log.info " [path: #{file_local_path}]" + + return file_local_path + end + + public + # upload package + def upload(ip, port, binary_path_list, ftp_addr=nil, ftp_port=nil, ftp_username=nil, ftp_passwd=nil) + + # check ip and port + if ip.nil? or port.nil? then + @log.error "Ip and port should be set." + return nil + end + + # check binary path list + if binary_path_list.nil? or binary_path_list.empty? then + @log.error "Binary package path should be set." + return nil + end + + # create unique dock number + dock = Utils.create_uniq_name() + + # upload file + binary_list = [] + binary_path_list.each do |bpath| + filename = File.basename(bpath) + client = BuildCommClient.create(ip, port, @log) + + if client.nil? then + @log.error "Failed to create BuildCommClient instance.." + return nil + end + + @log.info "Send ready REQ.. [UPLOAD]" + result = client.send("UPLOAD|#{dock}") + if not result then + @log.error "Failed to send ready REQ.." + return nil + end + + begin + if not ftp_addr.nil? then + transporter=FileTransferFTP.new(@log, ftp_addr, ftp_port, ftp_username, ftp_passwd) + else + transporter=FileTransferDirect.new(@log) + end + + result = client.send_file(bpath, transporter) + rescue => e + @log.error "FTP failed to put file (exception)" + @log.error "#{e.message}" + @log.error e.backtrace.inspect + return nil + end + + if not result then + @log.error "FTP failed to put file (result is false)" + return nil + end + + client.terminate + binary_list.push(filename) + end + + # register file + if not binary_list.empty? then + client = BuildCommClient.create(ip, port, @log) + dist = get_distribution + if dist.empty? then + @log.error "Distribution is empty.." + return nil + end + + @log.info "Send register message.. [REGISTER|#{dist}|DOCK|#{dock}|#{binary_list.join("|")}]" + snapshot = nil + if client.send "REGISTER|#{dist}|DOCK|#{dock}|#{binary_list.join("|")}" then + result = client.read_lines do |l| + line = l.split("|") + if line[0].strip == "ERROR" then + @log.error l.strip + break + elsif line[0].strip == "SUCC" then + snapshot = line[1].strip + end + end + + if not result or snapshot.nil? then + @log.error "Failed to register! #{client.get_error_msg()}" + return nil + end + end + + client.terminate + snapshot = @server_addr + "/snapshots/" + snapshot + @log.info "Registered successfully! [#{binary_path_list.join("|")}]" + if snapshot.empty? then + @log.error "Failed to generate snapshot" + end + end + + return snapshot + end + + private + # verify package before uploading + def verify_upload(pkg_name, pkg_path) + + manifest_file = "pkginfo.manifest" + uniq_name = Utils.create_uniq_name + path = Utils::HOME + "/tmp/#{uniq_name}" + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + begin + if not FileInstaller.extract_a_file(pkg_path, manifest_file, path, @log) then + @log.error "The \"pkginfo.manifest\" file does not exist in \"#{pkg_path}\"" + return false + end + manifest_path = File.join(path, manifest_file) + pkg = Parser.read_single_pkginfo_from manifest_path + if File.exists? manifest_path then FileUtils.rm_f(manifest_path) end + FileUtils.remove_dir(path, true) + rescue Interrupt + @log.error "Client: Interrupted.." + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + raise Interrupt + rescue RuntimeError => e + @log.error( e.message, Log::LV_USER) + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + return false + end + new_pkg_ver = pkg.version + new_pkg_install_dep_list = pkg.install_dep_list + os = pkg.os + + list = get_all_reverse_install_dependent_packages_remote(pkg_name, os, true) + + if not list.nil? then + list.each do |p| + ilist = get_attr_from_pkg(p, os, "install_dep_list") + if ilist.nil? then next end + ilist.each do |l| + if l.package_name.eql? pkg_name then + if not l.match? new_pkg_ver then + @log.error "\"#{p}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version})" + return false + end + end + end + end + end + + if not new_pkg_install_dep_list.nil? then + new_pkg_install_dep_list.each do |l| + if not check_remote_pkg(l.package_name, os) then + @log.error "\"#{pkg_name}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version}), but \"#{l.package_name}\" is not exist on server" + return false + end + rver = get_attr_from_pkg(l.package_name, os, "version") + if not l.match? rver then + @log.error "\"#{pkg_name}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version})" + return false + end end - end - pkg_svr = "#{server_home}/tizen_sdk/dev_tools/pkg-svr" - - # set incoming directory (~/.build_tools/pkg_server/#{id}/incoming) - incoming_path = "#{server_home}/.build_tools/pkg_server/#{id}/incoming" - - # set pkg-svr register command - register_command = "#{pkg_svr} register -i #{id} -d #{dist}" - - # upload source package (scp) - server_src_pkg_list_command = "\"" - source_path_list.each do |spath| - # set source package file path for server filesystem - src_file_name = File.basename(spath) - server_src_pkg_path = "#{incoming_path}/#{src_file_name}" - server_src_pkg_list_command = server_src_pkg_list_command + server_src_pkg_path + "," - # upload source package - if File.exist? spath then - system "scp #{spath} #{ssh_alias}:#{server_src_pkg_path}" - else - @log.error "#{spath} file does not exist" - return nil - end - end - - server_src_pkg_list_command = server_src_pkg_list_command.strip - if server_src_pkg_list_command.end_with? "," then - server_src_pkg_list_command = server_src_pkg_list_command.chop + "\"" - else - server_src_pkg_list_command = server_src_pkg_list_command + "\"" - end - - # add src package list to register command - register_command = register_command + " -s #{server_src_pkg_list_command} -g" - - # upload binary package (scp) - if not binary_path_list.nil? then - server_bin_pkg_list_command = "\"" - binary_path_list.each do |bpath| - bin_file_name = File.basename(bpath) - bin_pkg_name = bin_file_name.split("_")[0] - if verify then - if not verify_upload(bin_pkg_name, bpath) then - @log.error "Failed to verify \"#{bpath}\" file" - return nil - end - end - - server_bin_pkg_path = "#{incoming_path}/#{bin_file_name}" - server_bin_pkg_list_command = server_bin_pkg_list_command + server_bin_pkg_path + "," - # upload binary package - if File.exist? bpath then - Utils.execute_shell("cd #{File.dirname(bpath)};scp #{File.basename(bpath)} #{ssh_alias}:#{server_bin_pkg_path}") - else - @log.error "#{bpath} file does not exist" - return nil - end - end - - server_bin_pkg_list_command = server_bin_pkg_list_command.strip - if server_bin_pkg_list_command.end_with? "," then - server_bin_pkg_list_command = server_bin_pkg_list_command.chop + "\"" - else - server_bin_pkg_list_command = server_bin_pkg_list_command + "\"" - end - - # add bin package list to register command - register_command = register_command + " -p #{server_bin_pkg_list_command}" - end - - @log.info "register_command : #{register_command}" - - # register packages to server - result = `ssh #{ssh_alias} #{register_command}` - if result.strip.include? "Error occured" then - puts result - return nil - end - - # parsing snapshot url to show user - serveraddr = @server_addr - arr = serveraddr.split("/") - if serveraddr.include? "snapshots" then sid = arr[-4] - else sid = arr[-2] end - i = serveraddr.index(sid) - serveraddr = serveraddr[0..i-1] - serveraddr = serveraddr + id + "/" + dist - - addr = [] - result2 = "" - arr_re = result.split("\n") - arr_re.each do |l| - l = l.strip - if l.start_with? "snapshot is generated :" then - addr = l.split(":")[1].split("/") - if addr.include? dist then - i = addr.index(dist) - addr = addr[i+1..-1] - str = "" - addr.each do |l| str = str + "/" + l end - str = serveraddr.strip + str - result2 = result2 + str +"\n" - end - end - end - - @log.info "Upload packages.. OK" - @log.info " [#{binary_path_list.join(", ")}]" - @log.info " [#{source_path_list.join(", ")}]" - return result2 - end - - private - # verify package before uploading - def verify_upload(pkg_name, pkg_path) - - manifest_file = "pkginfo.manifest" - uniq_name = Utils.create_uniq_name - path = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{path}" - if not FileInstaller.extract_specified_file(pkg_path, manifest_file, path) then - @log.error "The \"pkginfo.manifest\" file does not exist in \"#{pkg_path}\"" - return false - end - manifest_path = File.join(path, manifest_file) - pkg_hash = Parser.read_pkg_list(manifest_path) - FileUtils.rm_f(manifest_path) - FileUtils.remove_dir(path, true) - - new_pkg_ver = pkg_hash[pkg_name].version - new_pkg_install_dep_list = pkg_hash[pkg_name].install_dep_list - os = pkg_hash[pkg_name].os - - list = get_all_reverse_install_dependent_packages_remote(pkg_name, os, true) - - if not list.nil? then - list.each do |p| - ilist = get_attr_from_pkg(p, os, "install_dep_list") - if ilist.nil? then next end - ilist.each do |l| - if l.package_name.eql? pkg_name then - if not l.match? new_pkg_ver then - @log.error "\"#{p}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version})" - return false - end - end - end - end - end - - if not new_pkg_install_dep_list.nil? then - new_pkg_install_dep_list.each do |l| - if not check_remote_pkg(l.package_name, os) then - @log.error "\"#{pkg_name}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version}), but \"#{l.package_name}\" is not exist on server" - return false - end - rver = get_attr_from_pkg(l.package_name, os, "version") - if not l.match? rver then - @log.error "\"#{pkg_name}\" package has following install dependency : #{l.package_name} (#{l.comp} #{l.base_version})" - return false - end - end - end - - @log.info "Passed to verify packages for uploading.. OK" - return true - end - - private - # get distribution - def get_distribution() - server = @server_addr - if server.nil? or server.empty? then - @log.error "Server addr is nil" - return nil - end - - dist = "" - server_arr = server.split("/") - if server_arr.include? "snapshots" then - i = server_arr.index("snapshots") - dist = server_arr[i-1] - else dist = File.basename(server) end - - return dist - end - - public + end + + @log.info "Passed to verify packages for uploading.. OK" + return true + end + + private + # get distribution + def get_distribution() + server = @server_addr + if server.nil? or server.empty? then + @log.error "Server addr is nil" + return nil + end + + dist = "" + dist = File.basename(server) + + return dist + end + + private + def get_flat_serveraddr() + server = @server_addr + if server.nil? or server.empty? then + @log.error "Server addr is nil" + @log.error "check sync_pkg_servers table pkgsvr_url column is null" + return "nil" + end + + server = server.delete ".:/@" + return server + end + + public # install package - # install all install dependency packages - def install(pkg_name, os, trace, force) - - if trace.nil? then trace = true end - if force.nil? then force = false end - - # check meta package - is_meta_pkg = check_meta_pkg(pkg_name, os) - if is_meta_pkg then trace = true end - - pkg_ver = get_attr_from_pkg(pkg_name, os, "version") - if pkg_ver.nil? or pkg_ver.empty? then - @log.error "#{pkg_name} package does not exist in remote package list" - return false - end - - compare_result = compare_version_with_installed_pkg(pkg_name, pkg_ver) - if not force then - case compare_result - when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version" - return true - when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version" - return true - when 1, 2 then - end - end - - # if enable trace, crate all dependent package list - if trace then - dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, force) - if dependent_pkg_list.nil? then - @log.error "Failed to get dependency for \"#{pkg_name}\" package" - return false - end - else - dependent_pkg_list = [pkg_name] - end - - # if meta package, dependent list does not need to include self name - #if is_meta_pkg then - # dependent_pkg_list.delete(pkg_name.strip) - #end - - # TODO: need to compare dependent package version - # install packages including dependent packages - dependent_pkg_list.each do |pkg| - if not install_pkg(pkg, os, force) then - @log.error "#{pkg} does not exist" - return false - end - add_pkg_info(pkg, os) - end - write_pkg_hash_to_file(nil) - - if trace then - @log.info "Installed \"#{pkg_name} [#{pkg_ver}]\" package with all dependent packages.. OK" - @log.info " [#{dependent_pkg_list.join(" -> ")}]" - else - @log.info "Install only \"#{pkg_name} [#{pkg_ver}]\" package.. OK" - end - return true - end - - public - # install local package (ignore dependent packages) - def install_local_pkg(pkg_path, force) - - file_name = File.basename(pkg_path) - pkg_name = file_name.split('_')[0] - - if not File.exist? pkg_path then - @log.error "\"#{pkg_path}\" file does not exist" - return false - end - filename = File.basename(pkg_path) - ext = File.extname(filename) - if not ext.eql? ".zip" then - @log.error "\"#{file_name}\" is not zip file. binary package file should have .zip ext" - return false - end - pkg_name = filename.split("_")[0] - type = "binary" - manifest_file = "pkginfo.manifest" - pkg_config_path = File.join(@location, PACKAGE_INFO_DIR, pkg_name) - - uniq_name = Utils.create_uniq_name - path = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{path}" - if not FileInstaller.extract_specified_file(pkg_path, manifest_file, path) then - @log.error "pkginfo.manifest file does not exist in #{pkg_path}" - return false - end - manifest_path = File.join(path, manifest_file) - pkg_hash = Parser.read_pkg_list(manifest_path) - new_pkg_ver = pkg_hash[pkg_name].version - FileUtils.rm_f(manifest_path) - FileUtils.remove_dir(path, true) - - compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - if not force then - case compare_result - when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version.." - return true - when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version.." - return true - when 1, 2 then - end - end - - if check_installed_pkg(pkg_name) then - uninstall(pkg_name, false) - end - - # install package - ret = FileInstaller.install(pkg_name, pkg_path, "binary", @location) - - add_local_pkg_info(pkg_name) - write_pkg_hash_to_file(nil) - - @log.info "Installed \"#{pkg_path} [#{new_pkg_ver}]\" file.. OK" - return true - end - - public - # upgrade package - def upgrade(os, trace) - - if trace.nil? then trace = true end - list = check_upgrade(os) - - if list.empty? or list.nil? then - @log.info "There is no packages for upgrading.." - return false - end - - list.each do |p| - if check_installed_pkg(p) then - if not uninstall(p, trace) then - @log.error "Failed to uninstall \"#{p}\" package.." - return false - end - end - - if not install(p, os, trace, false) then - @log.error "Failed to install \"#{p}\" package.." - return false - end - end - - @log.info "Upgraded packages from #{@server_addr}.. OK" - return true - end - - public - # check package which will be upgraded - def check_upgrade(os) - - update_pkgs = [] - installed_pkg_hash_key = get_installed_pkg_list_file_path() - installed_pkg_hash = installed_pkg_hash_loc[installed_pkg_hash_key] - remote_pkg_hash = pkg_hash_os[os] - - if remote_pkg_hash.nil? then - @log.error "There is no remote package list for #{os}. please pkg-cli update" - return nil - end - - if installed_pkg_hash.nil? then - @log.warn "There is no any installed package in \"#{@location}\"" - return remote_pkg_hash.keys - end - - arr_keys = installed_pkg_hash.keys - arr_keys.each do |k| - installed_ver = get_attr_from_installed_pkg(k, "version") - if not check_remote_pkg(k, os) then next end - remote_ver = get_attr_from_pkg(k, os, "version") - compare_result = compare_version_with_installed_pkg(k, remote_ver) - case compare_result - when -1 then next - when 0 then next - when 1 then - @log.output "\"#{k}\" package : #{installed_ver} -> #{remote_ver}" - update_pkgs.push(k) - end - end - - @log.info "Checked packages for upgrading.. OK" - return update_pkgs - end - - public - def get_default_server_addr() - filepath = "#{CONFIG_PATH}/config" - server_addr = nil - - if not File.exist? filepath then create_default_config(nil) end - if not File.exist? filepath then - @log.error "There is no default server address in #{filepath}" - return nil - end - - File.open filepath, "r" do |f| - f.each_line do |l| - if l.strip.start_with? "DEFAULT_SERVER_ADDR :" then - server_addr = l.split("DEFAULT_SERVER_ADDR :")[1].strip - break - else next end - end - end - - if server_addr.nil? then create_default_config(DEFAULT_SERVER_ADDR) end - return server_addr - end - - public - # get default path for installing - def get_default_inst_dir() - return Dir.pwd - end - - private - # create default config file (Utils::HOME/.build_tools/client/config) - def create_default_config(server_addr) - filepath = "#{CONFIG_PATH}/config" - if server_addr.nil? then server_addr = DEFAULT_SERVER_ADDR end - - if File.exist? filepath then - FileUtils.rm_f(filepath) - end - - if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end - - File.open(filepath, "a+") do |file| - file.puts "DEFAULT_SERVER_ADDR : #{server_addr}" - end - end - - public - # uninstall package - # trace : if true, uninstall all dependent packages - def uninstall(pkg_name, trace) - - type = "binary" - pkg_list = [] - pkg_hash = nil - - if not check_installed_pkg(pkg_name) then - @log.error "\"#{pkg_name}\" package is not installed." - return false - end - - pkg_ver = get_attr_from_installed_pkg(pkg_name, "version") - - if trace then - pkg_list = get_all_reverse_install_dependent_packages(pkg_name, true) - if pkg_list.nil? then - @log.error "Failed to get \"#{pkg_name}\" package dependency information." - return false - end - else - pkg_list.push(pkg_name) - end - - pkg_list.each do |p| - if not check_installed_pkg(p) then next end - if not FileInstaller.uninstall(p, type, @location) then - @log.error "Failed uninstall \"#{pkg_name}\" package" - return false - end - pkg_hash = remove_pkg_info(p) - end - - if trace then - @log.info "Uninstalled \"#{pkg_name} [#{pkg_ver}]\" package with all dependent packages.. OK" - @log.info " [#{pkg_list.join(" -> ")}]" - else - @log.info "Uninstalled only \"#{pkg_name} [#{pkg_ver}]\" package.. OK" - end - - write_pkg_hash_to_file(nil) - return true - end - - public - # clean - def clean(force) - if not force then - puts "Do you really want to remove \"#{@location}\" path? [yes]" - input = $stdin.gets.strip - if input.upcase.eql? "YES" then - @log.info "Removed \"#{@location}\"" - else - @log.info "Canceled" - return - end - end - FileUtils.rm_rf(@location) - FileUtils.mkdir_p(@location) - @pkg_hash_os.clear - @installed_pkg_hash_loc.clear - @archive_pkg_list.clear - @log.info "Cleaned \"#{@location}\" path.. OK" - end - - public + # install all install dependency packages + def install(pkg_name, os, trace, force) + + ret = install_internal( pkg_name, os, trace, force ) + return ret + end + + + private + def install_internal(pkg_name, os, trace, force) + + if trace.nil? then trace = true end + if force.nil? then force = false end + + # check meta package + is_meta_pkg = check_meta_pkg(pkg_name, os) + if is_meta_pkg then trace = true end + + # compare package version with installed package's + pkg_ver = get_attr_from_pkg(pkg_name, os, "version") + if pkg_ver.nil? or pkg_ver.empty? then + @log.error "#{pkg_name} package does not exist in remote package list" + return false + end + + + compare_result = compare_version_with_installed_pkg(pkg_name, pkg_ver) + if not force then + case compare_result + when -1 then + @log.warn "Checked \"#{pkg_name}\" package version : it is bigger then remote package version" + return true + when 0 then + @log.warn "Checked \"#{pkg_name}\" package version : it is same with remote package version" + return true + when 1, 2 then + end + end + + # if enable trace, create all dependent package list + if trace then + dependent_pkg_list = get_install_dependent_packages(pkg_name, os, true, force) + if dependent_pkg_list.nil? then + @log.error "Failed to get dependency for \"#{pkg_name}\" package" + return false + end + else + dependent_pkg_list = [pkg_name] + end + + # TODO: need to compare dependent package version + # install packages including dependent packages + dependent_pkg_list.each do |pkg| + if not install_pkg(pkg, os, force) then + @log.error "#{pkg} does not exist" + return false + end + add_pkg_info(pkg, os) + end + + # write installed package information to file + write_pkg_hash_to_file(nil) + + if trace then + @log.info "Installed \"#{pkg_name} [#{pkg_ver}]\" package with all dependent packages.. OK" + @log.info " [#{dependent_pkg_list.join(" -> ")}]" + else + @log.info "Install only \"#{pkg_name} [#{pkg_ver}]\" package.. OK" + end + + return true + end + + + public + # install local package (ignore dependent packages) + def install_local_pkg(pkg_path, trace, force, repos_paths = nil) + + ret = install_local_pkg_internal(pkg_path, trace, force, repos_paths) + return ret + end + + + private + def install_local_pkg_internal(pkg_path, trace, force, repos_paths) + + file_name = File.basename(pkg_path) + pkg_name = file_name.split('_')[0] + + if not File.exist? pkg_path then + @log.error "\"#{pkg_path}\" file does not exist" + return false + end + filename = File.basename(pkg_path) + ext = File.extname(filename) + if not ext.eql? ".zip" then + @log.error "\"#{file_name}\" is not zip file. binary package file should have .zip ext" + return false + end + pkg_name = filename.split("_")[0] + manifest_file = "pkginfo.manifest" + + uniq_name = Utils.create_uniq_name + path = Utils::HOME + "/tmp/#{uniq_name}" + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + begin + if not FileInstaller.extract_a_file(pkg_path, manifest_file, path, @log) then + @log.error "pkginfo.manifest file does not exist in #{pkg_path}" + return false + end + manifest_path = File.join(path, manifest_file) + pkg = Parser.read_single_pkginfo_from manifest_path + new_pkg_ver = pkg.version + FileUtils.remove_dir(path, true) + rescue Interrupt + @log.error "Client: Interrupted.." + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + raise Interrupt + rescue RuntimeError => e + @log.error( e.message, Log::LV_USER) + FileUtils.remove_dir(path, true) + @log.info "Removed #{path}" + return false + end + + compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + if not force then + case compare_result + when -1 then + @log.warn "Installed \"#{pkg_name}\" package version is bigger.." + return true + when 0 then + @log.warn "Checked \"#{pkg_name}\" package version : it is same with installed package version" + return true + when 1, 2 then + end + end + + if check_installed_pkg(pkg_name) then + uninstall(pkg_name, false) + end + + if trace then + install_dep_pkgs = pkg.install_dep_list + new_pkg_os = pkg.os + install_dep_pkgs.each do |p| + # check local path first + if not repos_paths.nil? then + # search + binpkgs = [] + repos_paths.each do |repos_path| + binpkgs += Dir.glob("#{repos_path}/#{p.package_name}_*_#{new_pkg_os}.zip") + end + if not binpkgs.empty? then + if not install_local_pkg_internal(binpkgs[0], true, false, repos_paths) then + @log.warn "#{p} package is not installed" + end + else + if not install_internal(p.package_name, new_pkg_os, true, false) then + @log.warn "#{p} package is not installed" + end + end + else + if not install_internal(p.package_name, new_pkg_os, true, false) then + @log.warn "#{p} package is not installed" + end + end + end + end + + # install package + ret = FileInstaller.install(pkg_name, pkg_path, "binary", @location, @log) + + if not ret then + @log.error "Install failed \"#{pkg_path} [#{new_pkg_ver}]\" file.. " + return false + end + + add_local_pkg_info(pkg_name) + write_pkg_hash_to_file(nil) + + @log.info "Installed \"#{pkg_path} [#{new_pkg_ver}]\" file.. OK" + return true + end + + + public + # upgrade package + def upgrade(os, trace) + + if trace.nil? then trace = true end + list = check_upgrade(os) + + if list.empty? or list.nil? then + @log.info "There is no packages for upgrading.." + return false + end + + list.each do |p| + if check_installed_pkg(p) then + if not uninstall(p, trace) then + @log.error "Failed to uninstall \"#{p}\" package.." + return false + end + end + + if not install_internal(p, os, trace, false) then + @log.error "Failed to install \"#{p}\" package.." + return false + end + end + + @log.info "Upgraded packages from #{@server_addr}.. OK" + return true + end + + public + # check package which will be upgraded + def check_upgrade(os) + + update_pkgs = [] + installed_pkg_hash_key = get_installed_pkg_list_file_path() + installed_pkg_hash = installed_pkg_hash_loc[installed_pkg_hash_key] + remote_pkg_hash = pkg_hash_os[os] + + if remote_pkg_hash.nil? then + @log.error "There is no remote package list for #{os}. please pkg-cli update" + return nil + end + + if installed_pkg_hash.nil? then + @log.warn "There is no any installed package in \"#{@location}\"" + return remote_pkg_hash.keys + end + + arr_keys = installed_pkg_hash.keys + arr_keys.each do |k| + installed_ver = get_attr_from_installed_pkg(k, "version") + if not check_remote_pkg(k, os) then next end + remote_ver = get_attr_from_pkg(k, os, "version") + compare_result = compare_version_with_installed_pkg(k, remote_ver) + case compare_result + when -1 then next + when 0 then next + when 1 then + @log.output "\"#{k}\" package : #{installed_ver} -> #{remote_ver}" + update_pkgs.push(k) + end + end + + @log.info "Checked packages for upgrading.. OK" + return update_pkgs + end + + public + def get_default_server_addr() + filepath = "#{CONFIG_PATH}/config" + server_addr = nil + + if not File.exist? filepath then create_default_config(nil) end + if not File.exist? filepath then + @log.error "There is no default server address in #{filepath}" + return nil + end + + File.open filepath, "r" do |f| + f.each_line do |l| + if l.strip.start_with? "DEFAULT_SERVER_ADDR :" then + server_addr = l.split("DEFAULT_SERVER_ADDR :")[1].strip + break + else next end + end + end + + if server_addr.nil? then create_default_config(DEFAULT_SERVER_ADDR) end + return server_addr + end + + public + # get default path for installing + def get_default_inst_dir() + return Dir.pwd + end + + private + # create default config file (Utils::HOME/.build_tools/client/config) + def create_default_config(server_addr) + filepath = "#{CONFIG_PATH}/config" + if server_addr.nil? then server_addr = DEFAULT_SERVER_ADDR end + + if File.exist? filepath then + FileUtils.rm_f(filepath) + end + + if server_addr.strip.end_with? "/" then server_addr = server_addr.chop end + + File.open(filepath, "a+") do |file| + file.puts "DEFAULT_SERVER_ADDR : #{server_addr}" + end + end + + public + # uninstall package + # trace : if true, uninstall all dependent packages + def uninstall(pkg_name, trace) + + type = "binary" + pkg_list = [] + pkg_hash = nil + + if not check_installed_pkg(pkg_name) then + @log.error "\"#{pkg_name}\" package is not installed." + return false + end + + pkg_ver = get_attr_from_installed_pkg(pkg_name, "version") + + if trace then + pkg_list = get_all_reverse_install_dependent_packages(pkg_name, true) + if pkg_list.nil? then + @log.error "Failed to get \"#{pkg_name}\" package dependency information." + return false + end + else + pkg_list.push(pkg_name) + end + + pkg_list.each do |p| + if not check_installed_pkg(p) then next end + if not FileInstaller.uninstall(p, type, @location, @log) then + @log.error "Failed uninstall \"#{pkg_name}\" package" + return false + end + pkg_hash = remove_pkg_info(p) + end + + if trace then + @log.info "Uninstalled \"#{pkg_name} [#{pkg_ver}]\" package with all dependent packages.. OK" + @log.info " [#{pkg_list.join(" -> ")}]" + else + @log.info "Uninstalled only \"#{pkg_name} [#{pkg_ver}]\" package.. OK" + end + + write_pkg_hash_to_file(nil) + return true + end + + public + # clean + def clean(force) + if not force then + puts "Do you really want to remove \"#{@location}\" path? [yes]" + input = $stdin.gets.strip + if input.upcase.eql? "YES" then + @log.info "Removed \"#{@location}\"" + else + @log.info "Canceled" + return + end + end + if File.exist? @location then FileUtils.rm_rf(@location) end + FileUtils.mkdir_p(@location) + #@pkg_hash_os.clear + @installed_pkg_hash_loc.clear + #@archive_pkg_list.clear + @log.info "Cleaned \"#{@location}\" path.. OK" + end + + public # get reverse build dependent packages (just 1 depth) - def get_reverse_build_dependent_packages(pkg_name, os) - - result = [] - pkg_hash = @pkg_hash_os[os] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.build_dep_list.each do |dep| - if dep.package_name.eql? pkg_name and - not dep.target_os_list.nil? and + def get_reverse_build_dependent_packages(pkg_name, os) + + result = [] + pkg_hash = @pkg_hash_os[os] + if pkg_hash.nil? then return [] end + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.build_dep_list.each do |dep| + if dep.package_name.eql? pkg_name and + not dep.target_os_list.nil? and dep.target_os_list.include? os then - result.push(pkg.package_name) - end - end - end + result.push(pkg) + end + end + end - return result - end + return result + end - public + public # get reverse source dependent packages (just 1 depth) - def get_reverse_source_dependent_packages(pkg_name, os) - - result = [] - pkg_hash = @pkg_hash_os[os] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.source_dep_list.each do |p| - if p.package_name.eql? pkg_name then - result.push(pkg.package_name) - end - end - end - - return result - end - - public + def get_reverse_source_dependent_packages(pkg_name) + + result = [] + @support_os_list.each do |os| + pkg_hash = @pkg_hash_os[os] + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.source_dep_list.each do |p| + if p.package_name.eql? pkg_name then + result.push(pkg) + end + end + end + end + + return result + end + + public # get reverse install dependent packages (jush 1 depth) - def get_reverse_install_dependent_packages(pkg_name, os) - - result = [] - pkg_hash = @pkg_hash_os[os] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.install_dep_list.each do |p| - if p.package_name.eql? pkg_name then - result.push(pkg.package_name) - end - end - end - - return result - end - - public + def get_reverse_install_dependent_packages(pkg_name, os) + + result = [] + pkg_hash = @pkg_hash_os[os] + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.install_dep_list.each do |p| + if p.package_name.eql? pkg_name then + result.push(pkg.package_name) + end + end + end + + return result + end + + public # get all build dependent packages (considered build priority, and reverse) - def get_build_dependent_packages(pkg_name, os, reverse) - - if not check_remote_pkg(pkg_name, os) then return nil end - if reverse.nil? then reverse = true end - - @all_dep_list.clear - begin - get_build_dependency_arr(pkg_name, os, 0) - # in case of cross build dependency - rescue SystemStackError - @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." - return nil - end - - max = 0 - @all_dep_list.each do |p| - if p[0].to_i > max then - max = p[0].to_i - else next end - end - - result = [] - i = 0 - while i <= max - @all_dep_list.each do |p| - if p[0].to_i.eql? i then - d = p[1] - remote_os = get_attr_from_pkg(d.package_name, os, "os") - remote_ver = get_attr_from_pkg(d.package_name, os, "version") - if not d.target_os_list.include? remote_os then - @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.target_os_list.to_s}, but \"#{d.package_name}\" (#{remote_os}) package is in server" - return nil - end - if not d.match? remote_ver then - @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.comp} #{d.base_version}, but \"#{d.package_name}\" (#{remote_ver}) package is in server" - return nil - else result.push(d.package_name) end - end - end - i = i + 1 - end - - @log.info "Get build dependent packages for #{pkg_name} package.. OK" - if reverse then return result.reverse.uniq.push(pkg_name) - else return result.uniq.insert(0, pkg_name) end - end - - public + def get_build_dependent_packages(pkg_name, os, reverse) + + if not check_remote_pkg(pkg_name, os) then return nil end + if reverse.nil? then reverse = true end + + @all_dep_list.clear + begin + get_build_dependency_arr(pkg_name, os, 0) + # in case of cross build dependency + rescue SystemStackError + @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." + return nil + end + + max = 0 + @all_dep_list.each do |p| + if p[0].to_i > max then + max = p[0].to_i + else next end + end + + result = [] + i = 0 + while i <= max + @all_dep_list.each do |p| + if p[0].to_i.eql? i then + d = p[1] + remote_os = get_attr_from_pkg(d.package_name, os, "os") + remote_ver = get_attr_from_pkg(d.package_name, os, "version") + if not d.target_os_list.include? remote_os then + @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.target_os_list.to_s}, but \"#{d.package_name}\" (#{remote_os}) package is in server" + return nil + end + if not d.match? remote_ver then + @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.comp} #{d.base_version}, but \"#{d.package_name}\" (#{remote_ver}) package is in server" + return nil + else result.push(d.package_name) end + end + end + i = i + 1 + end + + @log.info "Get build dependent packages for #{pkg_name} package.. OK" + if reverse then return result.reverse.uniq.push(pkg_name) + else return result.uniq.insert(0, pkg_name) end + end + + public # get all install dependent packages (considered install priority, reverse, and force) - # reverse : return reverse result - # force : install package force - def get_install_dependent_packages(pkg_name, os, reverse, force) - - if not check_remote_pkg(pkg_name, os) then return nil end - if reverse.nil? then reverse = true end - - @all_dep_list.clear - begin - get_install_dependency_arr(pkg_name, os, force, 0) - # in case of cross build dependency - rescue SystemStackError - @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." - return nil - end - - max = 0 - @all_dep_list.each do |p| - if p[0].to_i > max then - max = p[0].to_i - else next end - end - - result = [] - i = 0 - while i <= max - @all_dep_list.each do |p| - if p[0].to_i.eql? i then - d = p[1] - remote_ver = get_attr_from_pkg(d.package_name, os, "version") - if not d.match? remote_ver then - @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.comp} #{d.base_version}, but \"#{d.package_name}\" (#{remote_ver}) package is in server" - return nil - else result.push(d.package_name) end - end - end - i = i + 1 - end - - @log.info "Get install dependent packages for #{pkg_name} package.. OK" - if reverse then return result.reverse.uniq.push(pkg_name) - else return result.uniq.insert(0, pkg_name) end - end - - public - # get all reverse install dependent packages (considered reverse install priority for tracing uninstall) - def get_all_reverse_install_dependent_packages(pkg_name, reverse) - - if not check_installed_pkg(pkg_name) then return nil end - if reverse.nil? then reverse = true end - - begin - res = get_all_reverse_install_dependency_arr(pkg_name, 0) - rescue SystemStackError - @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." - return nil - end - res2 = res.split("::") - result = [] - res2.each do |r| - result.push(r.split(':')[1]) - end - - @log.info "Get all reverse install dependent packages for #{pkg_name} package.. OK" - if reverse then return result.reverse.uniq - else return result end - end - - public - # get all reverse remote dependent packages (considered reverse install priority for tracing uninstall) - def get_all_reverse_install_dependent_packages_remote(pkg_name, os, reverse) - #if not check_remote_pkg(pkg_name, os) then return nil end - if reverse.nil? then reverse = true end - - begin - res = get_all_reverse_install_dependency_arr_remote(pkg_name, os, 0) - rescue SystemStackError - @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." - return nil - end - res2 = res.split("::") - result = [] - res2.each do |r| - result.push(r.split(':')[1]) - end - - @log.info "Get all reverse install dependent packages for #{pkg_name} package.. OK" - if reverse then return result.reverse - else return result end - end - - public - # check package whether to exist in remote server - def check_remote_pkg(pkg_name, os) - - pkg_hash = @pkg_hash_os[os] - if pkg_hash.nil? then return false end - pkg = pkg_hash[pkg_name] - if pkg.nil? then - @log.warn "There is no \"#{pkg_name}\" remote package information in list" - return false - end - - return true - end - - public - # check package whether to exist in installed packages - def check_installed_pkg(pkg_name) - - installed_pkg_hash_key = get_installed_pkg_list_file_path() - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - if pkg_hash.nil? then return false end - pkg = pkg_hash[pkg_name] - - if pkg.nil? then return false end - return true - end - - public - # get attribute from installed package - def get_attr_from_installed_pkg(pkg_name, attr) - - if not check_installed_pkg(pkg_name) then return nil end - pkg = get_installed_pkg_from_list(pkg_name) - - if pkg.nil? then return nil end - - case attr - when "version" then return pkg.version - when "source" then return pkg.source - when "src_path" then return pkg.src_path - when "os" then return pkg.os - when "build_dep_list" then return pkg.build_dep_list - when "install_dep_list" then return pkg.install_dep_list - when "attribute" then return pkg.attribute - end - end - - public - # get attribute from remote package - def get_attr_from_pkg(pkg_name, os, attr) - - if not check_remote_pkg(pkg_name, os) then return nil end - pkg = get_pkg_from_list(pkg_name, os) - - if pkg.nil? then return nil end - - case attr - when "path" then return pkg.path - when "source" then return pkg.source - when "version" then return pkg.version - when "src_path" then return pkg.src_path - when "os" then return pkg.os - when "build_dep_list" then return pkg.build_dep_list - when "install_dep_list" then return pkg.install_dep_list - when "attribute" then return pkg.attribute - end - end - - public - # show a package information - def show_pkg_info(pkg_name, os) - if not check_remote_pkg(pkg_name, os) then - @log.error "\"#{pkg_name}\" package does not exist" - return "" - end - - pkg = get_pkg_from_list(pkg_name, os) - return pkg.to_s - end - - public - # show all packages information - def show_pkg_list(os) - pkg_hash = @pkg_hash_os[os] - if pkg_hash.nil? then - @log.error "\"#{os}\" package list does not exist" - return "" - end - - pkg_all_list = [] - pkg_list = pkg_hash.values - pkg_list.each do |p| - pkg_all_list.push([p.package_name, p.version, p.description]) - end - return pkg_all_list.sort - end - - public - # show installed package information - def show_installed_pkg_info(pkg_name) - - if not check_installed_pkg(pkg_name) then - @log.error "\"#{pkg_name}\" package does not exist" - return "" - end - - pkg = get_installed_pkg_from_list(pkg_name) - return pkg.to_s - end - - public - # show all installed packages information - def show_installed_pkg_list() - - file_path = get_installed_pkg_list_file_path() - pkg_hash = @installed_pkg_hash_loc[file_path] - if pkg_hash.nil? then - @log.error "Installed package list does not exist" - return - end - pkg_all_list = [] - pkg_list = pkg_hash.values - pkg_list.each do |p| - pkg_all_list.push([p.package_name, p.version, p.description]) - end - return pkg_all_list.sort - end - - private - def get_build_dependency_arr(pkg_name, os, n) - pkg_hash = @pkg_hash_os[os] - pkg = pkg_hash[pkg_name] - - if pkg.nil? then - @log.error "\"#{pkg_name}\" package does not exist in server. please check it" - return - end - - # if package is already installed, skip tracing dependency - if check_installed_pkg(pkg_name) then - # compare version with installed package version - new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") - compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - if compare_result == -1 or compare_result == 0 then return end - end - - pkg.build_dep_list.each do |l| - @all_dep_list.push([n, l]) - get_build_dependency_arr(l.package_name, os, n+1) - end - - return - end - - private - def get_install_dependency_arr(pkg_name, os, force, n) - - pkg_hash = @pkg_hash_os[os] - pkg = pkg_hash[pkg_name] - - if pkg.nil? then - @log.error "\"#{pkg_name}\" package does not exist in server. please check it" - return - end - - # if package is already installed, skip tracing dependency - if check_installed_pkg(pkg_name) then - # compare version with installed package version - new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") - compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - if not force then - if compare_result == -1 or compare_result == 0 then return end - end - end - - pkg.install_dep_list.each do |l| - @all_dep_list.push([n, l]) - get_install_dependency_arr(l.package_name, os, force, n+1) - end - - return - end - - private - def get_all_reverse_install_dependency_arr(pkg_name, n) - - s = "#{n}:#{pkg_name}" - installed_pkg_hash_key = get_installed_pkg_list_file_path() - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.install_dep_list.each do |l| - if l.package_name.eql? pkg_name then - s = s + "::" + get_all_reverse_install_dependency_arr(pkg.package_name, n+1) - end - end - end - - return s - end - - private - def get_all_reverse_install_dependency_arr_remote(pkg_name, os, n) - - s = "#{n}:#{pkg_name}" - pkg_hash = @pkg_hash_os[os] - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.install_dep_list.each do |l| - if l.package_name.eql? pkg_name then - s = s + "::" + get_all_reverse_install_dependency_arr_remote(pkg.package_name, os, n+1) - end - end - end - - return s - end - - private - def get_pkg_from_list(pkg_name, os) - - pkg_hash = @pkg_hash_os[os] - if pkg_hash.nil? then return nil end - pkg = pkg_hash[pkg_name] - - return pkg - end - - private - def get_installed_pkg_from_list(pkg_name) - - installed_pkg_hash_key = get_installed_pkg_list_file_path() - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - pkg = pkg_hash[pkg_name] - if pkg.nil? then return nil end - - return pkg - end - - private - def install_pkg(pkg_name, os, force) - - new_pkg_ver = "" - - # install remote server package file - if not check_remote_pkg(pkg_name, os) then - @log.error "\"#{pkg_name}\" package does not exist in remote server" - return false - end - path = get_attr_from_pkg(pkg_name, os, "path") - # type should be binary. type = "binary" - # below code should be changed - type = path.split('/')[-2] - new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") - - # compare version with installed package versiona - compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - if not force then - case compare_result - when -1 then - @log.warn "\"#{pkg_name}\" package version is bigger then remote package version" - return true - when 0 then - @log.warn "\"#{pkg_name}\" package version is same with remote package version" - return true - end - end - - if check_installed_pkg(pkg_name) then - uninstall(pkg_name, false) - end - - # download file - # change download location temporary (back to the origin path after downloading) - loc_back = @location - uniq_name = Utils.create_uniq_name - tmppath = Utils::HOME + "/tmp/#{uniq_name}" - FileUtils.mkdir_p "#{tmppath}" - @location = tmppath - file_local_path = download(pkg_name, os, false)[0] - @location = loc_back - if file_local_path.nil? then return false end - - # install package - ret = FileInstaller.install(pkg_name, file_local_path, type, @location) - FileUtils.rm_f(file_local_path) - FileUtils.remove_dir(tmppath, true) - return ret - end - - private - def compare_version_with_installed_pkg(pkg_name, new_pkg_ver) - - if check_installed_pkg_list_file() then - create_installed_pkg_hash() - if check_installed_pkg(pkg_name) then - installed_pkg_ver = get_attr_from_installed_pkg(pkg_name, "version") - compare_result = Utils.compare_version(installed_pkg_ver, new_pkg_ver) - return compare_result - end - end - - return 2 - end - - private - def remove_pkg_info(pkg_name) - - pkg_hash = {} - installed_pkg_hash_key = get_installed_pkg_list_file_path() - if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - if pkg_hash.include? pkg_name then - pkg_hash.delete(pkg_name) - end - @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - else return nil end - - @log.info "Removed information for \"#{pkg_name}\" package.. OK" - return pkg_hash - end - - private - def add_pkg_info(pkg_name, os) - - pkg_hash = {} - installed_pkg_hash_key = get_installed_pkg_list_file_path() - if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - pkg_hash[pkg_name] = get_pkg_from_list(pkg_name, os) - else pkg_hash[pkg_name] = get_pkg_from_list(pkg_name, os) end - @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - - @log.info "Added information for \"#{pkg_name}\" package.. OK" - return pkg_hash - end - - private - # add package manifest info - def add_local_pkg_info(pkg_name) - - config_path = File.join(@location, PACKAGE_INFO_DIR, "#{pkg_name}") - pkg = read_pkginfo_file(pkg_name, config_path) - - if pkg.nil? then - @log.error "Failed to read pkginfo.manifest file" - return nil - end - - pkg_hash = {} - installed_pkg_hash_key = get_installed_pkg_list_file_path() - if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then - pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] - pkg_hash[pkg_name] = pkg - else pkg_hash[pkg_name] = pkg end - @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - - @log.info "Added information for \"#{pkg_name}\" package.. OK" - return pkg_hash - end - - private - # read package manifet info - def read_pkginfo_file(pkg_name, path) - - file_path = File.join(path, "pkginfo.manifest") - pkg_hash = Parser.read_pkg_list(file_path) - - if pkg_hash.nil? then - @log.error "Failed to read manifest file : #{file_path}" - return nil - end - - @log.info "Added information for \"#{pkg_name}\" package.. OK" - return pkg_hash[pkg_name] - end - - private - # from_server : if true, update from server - def create_remote_pkg_hash(from_server) - - for os in SUPPORTED_OS - filename = PKG_LIST_FILE_PREFIX + os - file_url = @server_addr + "/" + filename - local_file_path = File.join(CONFIG_PATH, filename) - if from_server then - if not FileDownLoader.download(file_url, CONFIG_PATH) then - return false - end - end - local_file_path = File.join(CONFIG_PATH, filename) - if File.exist? local_file_path then - pkg_hash = Parser.read_pkg_list(local_file_path) - @pkg_hash_os[os] = pkg_hash - end - end - - filename = "archive_pkg_list" - file_url = @server_addr + "/" + filename - if from_server then - if not FileDownLoader.download(file_url, CONFIG_PATH) then - @log.warn "Server does not have \"#{filename}\" file. This error can be ignored." - end - end - local_file_path = File.join(CONFIG_PATH, filename) - if File.exist? local_file_path then - File.open(local_file_path, "r") do |f| - f.each_line do |l| - @archive_pkg_list.push(l.strip) - end - end - end - - return true - end - - private - # create installed package hash - def create_installed_pkg_hash() - - config_path = File.join(@location, PACKAGE_INFO_DIR) - if not File.directory? config_path then return end - - installed_pkg_hash_key = get_installed_pkg_list_file_path() - if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then return - else - file_path = installed_pkg_hash_key - if not File.exist? file_path then - #raise RuntimeError, "#{file_path} file does not exist" - return - end - pkg_hash = Parser.read_pkg_list(file_path) - @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash - end - end - - private - # check to exist installed package list file - def check_installed_pkg_list_file() - - if @location.nil? then raise RuntimeError, "#{@location} path does not exist" end - file_path = get_installed_pkg_list_file_path() - if File.exist? file_path then return true - else return false end - end - - private - # get installed package list file path - def get_installed_pkg_list_file_path() - - file_full_path = File.join(@location, PACKAGE_INFO_DIR, INSTALLED_PKG_LIST_FILE) - return file_full_path - end - - private - # write package hash to file - def write_pkg_hash_to_file(pkg_hash) - - file_path = get_installed_pkg_list_file_path() - if pkg_hash.nil? then - pkg_hash = @installed_pkg_hash_loc[file_path] - end - if not pkg_hash.nil? then - config_path = File.join(@location, PACKAGE_INFO_DIR) - FileUtils.mkdir_p "#{config_path}" - if File.exist? file_path then File.delete(file_path) end - File.open(file_path, "a+") do |file| - file.puts "ORIGIN : #{@server_addr}" - file.puts "\n" - pkg_list = pkg_hash.values - pkg_list.each do |pkg| - pkg.print_to_file(file) - file.puts "\n" - end - end - end - @log.info "Write package informations to \"#{file_path}\".. OK" - end - - private - def check_meta_pkg(pkg_name, os) - if not check_remote_pkg(pkg_name, os) then return false end - - attr = get_attr_from_pkg(pkg_name, os, "attribute") - if attr.nil? or attr.empty? then return false end - if attr[0].strip.upcase.eql? "META" then return true - else return false end - end + # reverse : return reverse result + # force : install package force + def get_install_dependent_packages(pkg_name, os, reverse, force) + + if not check_remote_pkg(pkg_name, os) then return nil end + if reverse.nil? then reverse = true end + + @all_dep_list.clear + begin + get_install_dependency_arr(pkg_name, os, force, 0) + # in case of cross build dependency + rescue SystemStackError + @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." + return nil + end + + max = 0 + @all_dep_list.each do |p| + if p[0].to_i > max then + max = p[0].to_i + else next end + end + + result = [] + i = 0 + while i <= max + @all_dep_list.each do |p| + if p[0].to_i.eql? i then + d = p[1] + remote_ver = get_attr_from_pkg(d.package_name, os, "version") + if not d.match? remote_ver then + @log.error "\"#{pkg_name}\" package needs \"#{d.package_name}\" #{d.comp} #{d.base_version}, but \"#{d.package_name}\" (#{remote_ver}) package is in server" + return nil + else result.push(d.package_name) end + end + end + i = i + 1 + end + + @log.info "Get install dependent packages for \"#{pkg_name}\" package.. OK" + if reverse then return result.reverse.uniq.push(pkg_name) + else return result.uniq.insert(0, pkg_name) end + end + + public + # get all reverse install dependent packages (considered reverse install priority for tracing uninstall) + def get_all_reverse_install_dependent_packages(pkg_name, reverse) + + if not check_installed_pkg(pkg_name) then return nil end + if reverse.nil? then reverse = true end + + begin + res = get_all_reverse_install_dependency_arr(pkg_name, 0) + rescue SystemStackError + @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." + return nil + end + res2 = res.split("::") + result = [] + res2.each do |r| + result.push(r.split(':')[1]) + end + + @log.info "Get all reverse install dependent packages for #{pkg_name} package.. OK" + if reverse then return result.reverse.uniq + else return result end + end + + public + # get all reverse remote dependent packages (considered reverse install priority for tracing uninstall) + def get_all_reverse_install_dependent_packages_remote(pkg_name, os, reverse) + #if not check_remote_pkg(pkg_name, os) then return nil end + if reverse.nil? then reverse = true end + + begin + res = get_all_reverse_install_dependency_arr_remote(pkg_name, os, 0) + rescue SystemStackError + @log.error "Failed to get dependency relation because #{pkg_name} package has cross install dependency." + return nil + end + res2 = res.split("::") + result = [] + res2.each do |r| + result.push(r.split(':')[1]) + end + + @log.info "Get all reverse install dependent packages for #{pkg_name} package.. OK" + if reverse then return result.reverse + else return result end + end + + public + # check package whether to exist in remote server + def check_remote_pkg(pkg_name, os) + + pkg_hash = @pkg_hash_os[os] + if pkg_hash.nil? then return false end + pkg = pkg_hash[pkg_name] + if pkg.nil? then + #@log.warn "There is no \"#{pkg_name}\" remote package information in list" + return false + end + + return true + end + + public + # check package whether to exist in installed packages + def check_installed_pkg(pkg_name) + + installed_pkg_hash_key = get_installed_pkg_list_file_path() + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + if pkg_hash.nil? then return false end + pkg = pkg_hash[pkg_name] + + if pkg.nil? then return false end + return true + end + + public + # get attribute from installed package + def get_attr_from_installed_pkg(pkg_name, attr) + + if not check_installed_pkg(pkg_name) then return nil end + pkg = get_installed_pkg_from_list(pkg_name) + + if pkg.nil? then return nil end + + case attr + when "version" then return pkg.version + when "source" then return pkg.source + when "src_path" then return pkg.src_path + when "os" then return pkg.os + when "build_dep_list" then return pkg.build_dep_list + when "install_dep_list" then return pkg.install_dep_list + when "attribute" then return pkg.attribute + end + end + + public + # get attribute from remote package + def get_attr_from_pkg(pkg_name, os, attr) + + if not check_remote_pkg(pkg_name, os) then return nil end + pkg = get_pkg_from_list(pkg_name, os) + + if pkg.nil? then return nil end + + case attr + when "name" then return pkg.package_name + when "path" then return pkg.path + when "source" then return pkg.source + when "version" then return pkg.version + when "src_path" then return pkg.src_path + when "os" then return pkg.os + when "build_dep_list" then return pkg.build_dep_list + when "install_dep_list" then return pkg.install_dep_list + when "attribute" then return pkg.attribute + when "checksum" then return pkg.checksum + when "size" then return pkg.size + + end + end + + public + # show a package information + def show_pkg_info(pkg_name, os) + if not check_remote_pkg(pkg_name, os) then + @log.error "\"#{pkg_name}\" package does not exist" + return "" + end + + pkg = get_pkg_from_list(pkg_name, os) + return pkg.to_s + end + + public + # show all packages information + def show_pkg_list(os) + pkg_hash = @pkg_hash_os[os] + if pkg_hash.nil? then + @log.error "\"#{os}\" package list does not exist" + return "" + end + + pkg_all_list = [] + pkg_list = pkg_hash.values + pkg_list.each do |p| + pkg_all_list.push([p.package_name, p.version, p.description]) + end + return pkg_all_list.sort + end + + public + # show installed package information + def show_installed_pkg_info(pkg_name) + + if not check_installed_pkg(pkg_name) then + @log.error "\"#{pkg_name}\" package does not exist" + return "" + end + + pkg = get_installed_pkg_from_list(pkg_name) + return pkg.to_s + end + + public + # show all installed packages information + def show_installed_pkg_list() + + file_path = get_installed_pkg_list_file_path() + pkg_hash = @installed_pkg_hash_loc[file_path] + if pkg_hash.nil? then + @log.error "Installed package list does not exist" + return nil + end + pkg_all_list = [] + pkg_list = pkg_hash.values + pkg_list.each do |p| + pkg_all_list.push([p.package_name, p.version, p.description]) + end + return pkg_all_list.sort + end + + private + def get_build_dependency_arr(pkg_name, os, n) + pkg_hash = @pkg_hash_os[os] + pkg = pkg_hash[pkg_name] + + if pkg.nil? then + @log.error "\"#{pkg_name}\" package does not exist in server. please check it" + return + end + + # if package is already installed, skip tracing dependency + if check_installed_pkg(pkg_name) then + # compare version with installed package version + new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") + compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + if compare_result == -1 or compare_result == 0 then return end + end + + pkg.build_dep_list.each do |l| + @all_dep_list.push([n, l]) + get_build_dependency_arr(l.package_name, os, n+1) + end + + return + end + + private + def get_install_dependency_arr(pkg_name, os, force, n) + + pkg_hash = @pkg_hash_os[os] + pkg = pkg_hash[pkg_name] + + if pkg.nil? then + @log.error "\"#{pkg_name}\" package does not exist in server. please check it" + return + end + + # if package is already installed, skip tracing dependency + if check_installed_pkg(pkg_name) then + # compare version with installed package version + new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") + compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + if not force then + if compare_result == -1 or compare_result == 0 then return end + end + end + + pkg.install_dep_list.each do |l| + @all_dep_list.push([n, l]) + get_install_dependency_arr(l.package_name, os, force, n+1) + end + + return + end + + private + def get_all_reverse_install_dependency_arr(pkg_name, n) + + s = "#{n}:#{pkg_name}" + installed_pkg_hash_key = get_installed_pkg_list_file_path() + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.install_dep_list.each do |l| + if l.package_name.eql? pkg_name then + s = s + "::" + get_all_reverse_install_dependency_arr(pkg.package_name, n+1) + end + end + end + + return s + end + + private + def get_all_reverse_install_dependency_arr_remote(pkg_name, os, n) + + s = "#{n}:#{pkg_name}" + pkg_hash = @pkg_hash_os[os] + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.install_dep_list.each do |l| + if l.package_name.eql? pkg_name then + s = s + "::" + get_all_reverse_install_dependency_arr_remote(pkg.package_name, os, n+1) + end + end + end + + return s + end + + public + def get_pkg_from_list(pkg_name, os) + + pkg_hash = @pkg_hash_os[os] + if pkg_hash.nil? then return nil end + + pkg = pkg_hash[pkg_name] + + return pkg + end + + private + def get_installed_pkg_from_list(pkg_name) + + installed_pkg_hash_key = get_installed_pkg_list_file_path() + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + pkg = pkg_hash[pkg_name] + if pkg.nil? then return nil end + + return pkg + end + + private + # install a package to @location after uninstalling and downloading + def install_pkg(pkg_name, os, force) + + new_pkg_ver = "" + + # install remote server package file + if not check_remote_pkg(pkg_name, os) then + @log.error "\"#{pkg_name}\" package does not exist in remote server" + return false + end + path = get_attr_from_pkg(pkg_name, os, "path") + # type should be binary. type = "binary" + # below code should be changed + type = path.split('/')[-2] + new_pkg_ver = get_attr_from_pkg(pkg_name, os, "version") + pkg_checksum = get_attr_from_pkg(pkg_name, os, "checksum") + pkg_size = get_attr_from_pkg(pkg_name, os, "size") + pkg_path = get_attr_from_pkg(pkg_name, os, "path") + filename = pkg_path.split('/')[-1] + + # compare version with installed package versiona + compare_result = compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + if not force then + case compare_result + when -1 then + @log.warn "Checked \"#{pkg_name}\" package version : it is bigger then remote package version" + return true + when 0 then + @log.warn "Checked \"#{pkg_name}\" package version : it is same with remote package version" + return true + end + end + + # if package is already installed, then uninstall it + if check_installed_pkg(pkg_name) then + if not uninstall(pkg_name, false) then + @log.error "Failed to uninstall \"#{pkg_name}\"" + return false + end + end + + # install package + cached_filepath = nil + if Utils.is_linux_like_os( Utils::HOST_OS ) then + cached_filepath = get_cached_filepath(filename, pkg_checksum, pkg_size) + end + if not cached_filepath.nil? then + @log.info "Cached #{pkg_name} package file.. OK" + ret = FileInstaller.install(pkg_name, cached_filepath, type, @location, @log) + else + filepath = download(pkg_name, os, false, @tmp_path) + if filepath.nil? then + return false + end + filepath = move_downloaded_pkg(filepath[0], @download_path) + if filepath.nil? then + return false + end + ret = FileInstaller.install(pkg_name, filepath, type, @location, @log) + remove_downloaded_pkgs(pkg_name, os) + end + return ret + end + + private + def compare_version_with_installed_pkg(pkg_name, new_pkg_ver) + + if check_installed_pkg_list_file() then + read_installed_pkg_list() + if check_installed_pkg(pkg_name) then + installed_pkg_ver = get_attr_from_installed_pkg(pkg_name, "version") + compare_result = Utils.compare_version(installed_pkg_ver, new_pkg_ver) + return compare_result + end + end + + return 2 + end + + private + def remove_pkg_info(pkg_name) + + pkg_hash = {} + installed_pkg_hash_key = get_installed_pkg_list_file_path() + if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + if pkg_hash.include? pkg_name then + pkg_hash.delete(pkg_name) + end + @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash + else return nil end + + @log.info "Removed information for \"#{pkg_name}\" package.. OK" + return pkg_hash + end + + private + def add_pkg_info(pkg_name, os) + + pkg_hash = {} + installed_pkg_hash_key = get_installed_pkg_list_file_path() + if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + pkg_hash[pkg_name] = get_pkg_from_list(pkg_name, os) + else pkg_hash[pkg_name] = get_pkg_from_list(pkg_name, os) end + @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash + + #@log.info "Added information for \"#{pkg_name}\" package.. OK" + return pkg_hash + end + + private + # add package manifest info + def add_local_pkg_info(pkg_name) + + config_path = File.join(@location, PACKAGE_INFO_DIR, "#{pkg_name}") + pkg = read_pkginfo_file(pkg_name, config_path) + + if pkg.nil? then + @log.error "Failed to read pkginfo.manifest file" + return nil + end + + pkg_hash = {} + installed_pkg_hash_key = get_installed_pkg_list_file_path() + if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then + pkg_hash = @installed_pkg_hash_loc[installed_pkg_hash_key] + pkg_hash[pkg_name] = pkg + else pkg_hash[pkg_name] = pkg end + @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash + + #@log.info "Added information for \"#{pkg_name}\" package.. OK" + return pkg_hash + end + + private + # read package manifet info + def read_pkginfo_file(pkg_name, path) + + file_path = File.join(path, "pkginfo.manifest") + begin + pkg = Parser.read_single_pkginfo_from file_path + rescue => e + @log.error( e.message, Log::LV_USER) + return nil + end + + if pkg.nil? then + @log.error "Failed to read manifest file : #{file_path}" + return nil + end + + @log.info "Read information for \"#{pkg_name}\" package.. OK" + return pkg + end + + # get the lastest snapshot + # from_server : if true, update from server + def get_lastest_snapshot(from_server) + ssinfo_file = "snapshot.info" + file_url = File.join(@server_addr, ssinfo_file) + if from_server then + if not FileDownLoader.download(file_url, @config_dist_path, @log) then + @log.warn "Server does not have \"#{ssinfo_file}\" file. This error can be ignored." + end + else + if File.exist? file_url then FileUtils.cp(file_url, @config_dist_path) + else @log.warn "Server does not have \"#{ssinfo_file}\" file. This error can be ignored." end + end + + file_path = File.join(@config_dist_path, ssinfo_file) + if not File.exist? file_path then return nil end + + contents = File.open(file_path, "r").read + + _list = contents.split("\n\n") + if _list.nil? or _list == "" or _list.empty? then return nil end + list = _list[-1].split("\n") + if list.nil? or list == "" or list.empty? then return nil end + _path = list[-1].split(":") + if _path.nil? or _path == "" or _path.length != 2 then return nil end + path = _path[1].strip + if path == nil or path == "" then return nil end + + return path + end + + def get_pkglist_path() + return File.join(@config_dist_path, @snapshot_path) + end + + # if url includes snapshot infomation, retuen true + def is_snapshot_url(addr = nil) + if addr.nil? then addr = @server_addr end + addr_arr = addr.split('/') + if addr_arr[-2].eql? "snapshots" then + return true + else + return false + end + end + + def split_addr_and_snapshot(addr = nil) + if addr.nil? then addr = @server_addr end + addr_arr = addr.split('/') + if addr_arr[-2].eql? "snapshots" then + return addr_arr[0..-3].join("/"), addr_arr[-2..-1].join("/") + else + return nil + end + end + + def is_snapshot_exist(ss_path = nil) + if ss_path.nil? then ss_path = @snapshot_path + elsif ss_path == "" then return false end + + local_snapshot_path = File.join(@config_dist_path, ss_path) + if File.directory? local_snapshot_path then return true + else return false end + end + + def read_remote_pkg_list(list_path) + @support_os_list.each do |os| + filename = PKG_LIST_FILE_PREFIX + os + local_file_path = File.join(list_path, filename) + if File.exist? local_file_path then + begin + pkg_hash = Parser.read_repo_pkg_list_from local_file_path + @pkg_hash_os[os] = pkg_hash + @log.info "Get package information for #{os}.. OK" + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = {} + end + else + @log.warn "Failed to read pkg_list_#{os} file" + @pkg_hash_os[os] = {} + end + end + end + + def read_supported_os_list(list_path) + local_file_path = File.join(list_path, OS_INFO_FILE) + if File.exist? local_file_path then + File.open(local_file_path, "r") do |f| + f.each_line do |l| + os = l.strip + if @support_os_list.index(os).nil? then @support_os_list.push(os) end + end + end + @log.info "Get supported os infomation.. OK" + else + @log.warn "Failed to get supported os infomation" + end + end + + def download_os_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + file_url = File.join(@server_addr, OS_INFO_FILE) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + + return true + end + + def read_archive_pkg_list(list_path) + local_file_path = File.join(list_path, ARCHIVE_PKG_LIST_FILE) + if File.exist? local_file_path then + File.open(local_file_path, "r") do |f| + f.each_line do |l| + pkg = l.strip + if @archive_pkg_list.index(pkg).nil? then @archive_pkg_list.push(pkg) end + end + end + @log.info "Get archive package infomation.. OK" + else + @log.warn "Failed to get archive package infomation" + end + end + + def download_archive_pkg_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + file_url = File.join(@server_addr, @snapshot_path, ARCHIVE_PKG_LIST_FILE) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + + return true + end + + def download_pkg_list(from_server, dist = nil) + if dist.nil? then dist = get_pkglist_path end + @support_os_list.each do |os| + filename = PKG_LIST_FILE_PREFIX + os + file_url = File.join(@server_addr, @snapshot_path, filename) + if from_server then + if not FileDownLoader.download(file_url, dist, @log) then return false end + else + if File.exist? file_url then FileUtils.cp(file_url, dist) + else return false end + end + end + + return true + end + + private + # create installed package hash + def read_installed_pkg_list() + + config_path = File.join(@location, PACKAGE_INFO_DIR) + if not File.directory? config_path then return end + + installed_pkg_hash_key = get_installed_pkg_list_file_path() + if @installed_pkg_hash_loc.has_key? installed_pkg_hash_key then return + else + file_path = installed_pkg_hash_key + if not File.exist? file_path then + #raise RuntimeError, "#{file_path} file does not exist" + return + end + begin + pkg_hash = Parser.read_repo_pkg_list_from file_path + rescue => e + @log.error( e.message, Log::LV_USER) + return + end + @installed_pkg_hash_loc[installed_pkg_hash_key] = pkg_hash + end + end + + private + # check to exist installed package list file + def check_installed_pkg_list_file() + + if @location.nil? then raise RuntimeError, "#{@location} path does not exist" end + file_path = get_installed_pkg_list_file_path() + if File.exist? file_path then return true + else return false end + end + + private + # get installed package list file path + def get_installed_pkg_list_file_path() + + file_full_path = File.join(@location, PACKAGE_INFO_DIR, INSTALLED_PKG_LIST_FILE) + return file_full_path + end + + private + # write package hash to file + def write_pkg_hash_to_file(pkg_hash) + + file_path = get_installed_pkg_list_file_path() + if pkg_hash.nil? then + pkg_hash = @installed_pkg_hash_loc[file_path] + end + if not pkg_hash.nil? then + config_path = File.join(@location, PACKAGE_INFO_DIR) + if not File.exist? config_path then FileUtils.mkdir_p "#{config_path}" end + if File.exist? file_path then File.delete(file_path) end + File.open(file_path, "a+") do |file| + pkg_list = pkg_hash.values + pkg_list.each do |pkg| + pkg.print_to_file(file) + file.puts "\n" + end + end + end + @log.info "Write package informations to \"#{file_path}\".. OK" + end + + private + def check_meta_pkg(pkg_name, os) + if not check_remote_pkg(pkg_name, os) then return false end + + attr = get_attr_from_pkg(pkg_name, os, "attribute") + if attr.nil? or attr.empty? then return false end + if attr[0].strip.upcase.eql? "META" then return true + else return false end + end end diff --git a/src/pkg_server/clientOptParser.rb b/src/pkg_server/clientOptParser.rb index ae12c36..fff8924 100644 --- a/src/pkg_server/clientOptParser.rb +++ b/src/pkg_server/clientOptParser.rb @@ -1,5 +1,5 @@ =begin - + clientOptParser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -30,194 +30,177 @@ require 'optparse' $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "utils" -def set_default( options ) +def set_default( options ) if options[:t].nil? then options[:t] = false end if options[:f].nil? then options[:f] = false end if options[:v].nil? then options[:v] = false end end def option_error_check( options ) - $log.info "option error check" case options[:cmd] - when "update" then - - when "clean" then - - when "upgrade" then + when "update" then - when "check-upgrade" then + when "clean" then - when "download" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli download -p [-o ] [-l ] [-u ] [-t]" - end + when "upgrade" then - when "upload" then - if options[:alias].nil? or options[:alias].empty? or\ - options[:id].nil? or options[:id].empty? or \ - options[:srcpkg].nil? or options[:srcpkg].empty? then - raise ArgumentError, "Usage: pkg-cli upload -a -i -s [-b ]" - end + when "check-upgrade" then - when "source" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli source -p [-o ] [-l ] [-u ]" - end + when "download" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli download -P [-o ] [-l ] [-u ] [--trace]" + end - when "install" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install -p [-o ] [-l ] [-u ] [-t] [-f]" - end + when "install" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli install -P [-o ] [-l ] [-u ] [--trace] [--force]" + end - when "install-file" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install-lpkg -p [-l ] [-f]" - end + when "install-file" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli install-lpkg -P [-l ] [-u ] [--trace] [--force]" + end - when "uninstall" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli uninstall -p [-l ] [-t]" - end + when "uninstall" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli uninstall -P [-l ] [--trace]" + end - when "show-rpkg" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli show-rpkg -p [-o ] [-u ]" - end + when "show-rpkg" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli show-rpkg -P [-o ] [-u ]" + end - when "list-rpkg" then + when "list-rpkg" then - when "show-lpkg" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli show-lpkg -p [-l ]" - end + when "show-lpkg" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli show-lpkg -P [-l ]" + end - when "list-lpkg" then + when "list-lpkg" then - when "build-dep" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli build-dep -p [-o ]" - end + when "build-dep" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli build-dep -P [-o ]" + end - when "install-dep" then - if options[:pkg].nil? or options[:pkg].empty? then - raise ArgumentError, "Usage: pkg-cli install-dep -p [-o ]" - end + when "install-dep" then + if options[:pkg].nil? or options[:pkg].empty? then + raise ArgumentError, "Usage: pkg-cli install-dep -P [-o ]" + end else - raise ArgumentError, "input option incorrect : #{options[:cmd]}" + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" end end -def option_parse - options = {} - optparse = OptionParser.new do|opts| - # Set a banner, displayed at the top - # of the help screen. - opts.banner = "Usage: pkg-cli {update|clean|download|source|install|uninstall|upgrade|rpkg-show|rpkg-list|lpkg-show|lpkg-list|build-dep|install-dep|help} ..." + "\n" \ - + "\t" + "pkg-cli update [-u ]" + "\n" \ - + "\t" + "pkg-cli clean [-l ] [-f]" + "\n" \ - + "\t" + "pkg-cli download -p [-o ] [-l ] [-u ] [-t]" + "\n" \ - + "\t" + "pkg-cli upload -a -i -s [-b [-o ] [-l ] [-u ]" + "\n" \ - + "\t" + "pkg-cli install -p [-o ] [-l ] [-u ] [-t] [-f]" + "\n" \ - + "\t" + "pkg-cli install-file -p [-l ] [-f]" + "\n" \ - + "\t" + "pkg-cli uninstall -p [-l ] [-t]" + "\n" \ - + "\t" + "pkg-cli upgrade [-l ] [-o ] [-u ] [-t]" + "\n" \ - + "\t" + "pkg-cli check-upgrade [-l ] [-o ] [-u ]" + "\n" \ - + "\t" + "pkg-cli show-rpkg -p [-o ] [-u ]" + "\n" \ - + "\t" + "pkg-cli list-rpkg [-o ] [-u ]" + "\n" \ - + "\t" + "pkg-cli show-lpkg -p [-l ]" + "\n" \ - + "\t" + "pkg-cli list-lpkg [-l ]" + "\n" \ - + "\t" + "pkg-cli build-dep -p [-o ]" + "\n" \ - + "\t" + "pkg-cli install-dep -p [-o ]" + "\n" \ - - opts.on( '-p', '--pkg ', 'package name or package file name' ) do |name| - options[:pkg] = name - end - - opts.on( '-o', '--os ', 'target operating system' ) do |os| - options[:os] = os - end - - opts.on( '-u', '--url ', 'package server url' ) do|url| - options[:url] = url - end - - opts.on( '-a', '--alias ', 'ssh alias' ) do|al| - options[:alias] = al - end - - opts.on( '-i', '--id ', 'id' ) do|id| - options[:id] = id - end - - opts.on( '-l', '--loc ', 'location' ) do |loc| - options[:loc] = loc - end - - opts.on( '-s', '--src ', 'source package path' ) do|src| - options[:srcpkg] = [] - list = src.tr(" \t","").split(",") - list.each do |l| - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:srcpkg].push l - end - end - - opts.on( '-t', '--trace', 'enable trace dependent packages' ) do - options[:t] = true - end - - opts.on( '-b', '--bin ', 'binary package path' ) do|bin| - options[:binpkg] = [] - list = bin.tr(" \t","").split(",") - list.each do |l| - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:binpkg].push l - end - end - - opts.on( '-f', '--force', 'enable force' ) do - options[:f] = true - end - - opts.on( '-h', '--help', 'display this information' ) do - puts opts +def option_parse + options = {} + banner = "Requiest service to package-server and control packages service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-cli [OPTS] or pkg-cli (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "update Update to the latest package in your SDK environment." + "\n" \ + + "\t" + "clean Delete the package in your SDK environment." + "\n" \ + + "\t" + "download Download the package in your SDK environment." + "\n" \ + + "\t" + "install Download the package from package-server and install the package in your SDK environment." + "\n" \ + + "\t" + "install-file Install the package in your SDK environment." + "\n" \ + + "\t" + "uninstall Uninstall the package in your SDK environment." + "\n" \ + + "\t" + "upgrade Upgrade your SDK environment." + "\n" \ + + "\t" + "check-upgrade Check packages to upgrade." + "\n" \ + + "\t" + "show-rpkg Show the package in the package-server." + "\n" \ + + "\t" + "list-rpkg Show the all packages in the package-server." + "\n" \ + + "\t" + "show-lpkg Show the package in your SDK environment." + "\n" \ + + "\t" + "list-lpkg Show the all packages in your SDK environment." + "\n" \ + + "\t" + "build-dep Show build-dependency packages" + "\n" \ + + "\t" + "install-dep Show install-dependency packages" + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "pkg-cli update [-u ]" + "\n" \ + + "\t" + "pkg-cli clean [-l ] [--force]" + "\n" \ + + "\t" + "pkg-cli download -P [-o ] [-l ] [-u ] [--trace]" + "\n" \ + + "\t" + "pkg-cli install -P [-o ] [-l ] [-u ] [--trace] [--force]" + "\n" \ + + "\t" + "pkg-cli install-file -P [-l ] [-u ] [--trace] [--force]" + "\n" \ + + "\t" + "pkg-cli uninstall -P [-l ] [--trace]" + "\n" \ + + "\t" + "pkg-cli upgrade [-l ] [-o ] [-u ] [--trace]" + "\n" \ + + "\t" + "pkg-cli check-upgrade [-l ] [-o ] [-u ]" + "\n" \ + + "\t" + "pkg-cli show-rpkg -P [-o ] [-u ]" + "\n" \ + + "\t" + "pkg-cli list-rpkg [-o ] [-u ]" + "\n" \ + + "\t" + "pkg-cli show-lpkg -P [-l ]" + "\n" \ + + "\t" + "pkg-cli list-lpkg [-l ]" + "\n" \ + + "\t" + "pkg-cli build-dep -P [-o ]" + "\n" \ + + "\t" + "pkg-cli install-dep -P [-o ]" + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + # Set a banner, displayed at the top + # of the help screen. + + opts.banner = banner + + opts.on( '-P', '--pkg ', 'package name or package file name' ) do |name| + options[:pkg] = name + end + + opts.on( '-o', '--os ', 'target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64' ) do |os| + options[:os] = os + end + + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do |url| + options[:url] = url + end + + opts.on( '-l', '--loc ', 'install/download location' ) do |loc| + options[:loc] = loc + end + + opts.on( '--trace', 'enable trace dependent packages' ) do + options[:t] = true + end + + opts.on( '--force', 'enable force' ) do + options[:f] = true + end + + opts.on( '-h', '--help', 'display help' ) do + puts opts exit - end - - end - - $log.info "option parsing start" - $log.info "option is : " + ARGV * "," - - cmd = ARGV[0] - if cmd.eql? "update" or cmd.eql? "download" or \ - cmd.eql? "install" or cmd.eql? "show-rpkg" or \ - cmd.eql? "list-rpkg" or cmd.eql? "source" or \ - cmd.eql? "uninstall" or cmd.eql? "show-lpkg" or \ - cmd.eql? "list-lpkg" or cmd.eql? "upload" or \ - cmd.eql? "install-file" or cmd.eql? "clean" or \ - cmd.eql? "upgrade" or cmd.eql? "check-upgrade" or \ - cmd.eql? "build-dep" or cmd.eql? "install-dep" or \ - cmd =~ /(help)|(-h)|(--help)/ then - if cmd.eql? "help" then ARGV[0] = "-h" end - options[:cmd] = ARGV[0] - else - raise ArgumentError, "first paramter must be {update|clean|download|upload|source|install|install-file|uninstall|upgrade|check-upgrade|show-rpkg|list-rpkg|show-lpkg|list-lpkg|build-dep|install-dep|help} : your input is #{ARGV[0]}" - end - - optparse.parse! - - $log.info "option parsing end" + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end + end + + cmd = ARGV[0] + if cmd.eql? "update" or cmd.eql? "download" or + cmd.eql? "install" or cmd.eql? "show-rpkg" or + cmd.eql? "list-rpkg" or + cmd.eql? "uninstall" or cmd.eql? "show-lpkg" or + cmd.eql? "list-lpkg" or + cmd.eql? "install-file" or cmd.eql? "clean" or + cmd.eql? "upgrade" or cmd.eql? "check-upgrade" or + cmd.eql? "build-dep" or cmd.eql? "install-dep" or + cmd =~ /(-v)|(--version)/ or + cmd =~ /(help)|(-h)|(--help)/ then + + if cmd.eql? "help" then + V[0] = "-h" + end + options[:cmd] = ARGV[0] + else + raise ArgumentError, "Usage: pkg-cli [OPTS] or pkg-cli -h" + end + + optparse.parse! set_default options - # option error check + # option error check option_error_check options - return options -end + return options +end diff --git a/src/pkg_server/distribution.rb b/src/pkg_server/distribution.rb index 09f8da1..5aa613b 100644 --- a/src/pkg_server/distribution.rb +++ b/src/pkg_server/distribution.rb @@ -1,6 +1,6 @@ =begin - - distribution.rb + + distribution.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -29,75 +29,88 @@ Contributors: require 'fileutils' $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "parser" +require "installer" -class Distribution - attr_accessor :name, :location, :server_url +class Distribution + attr_accessor :name, :location, :server_url, :lock_file_path, :last_sync_changes - # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] - PKG_LIST_FILE_PREFIX = "pkg_list_" - ARCHIVE_PKG_LIST = "archive_pkg_list" + # constant + PKG_LIST_FILE_PREFIX = "pkg_list_" + ARCHIVE_PKG_FILE = "archive_pkg_list" + OS_INFO_FILE = "os_info" + SNAPSHOT_INFO_FILE = "snapshot.info" + LOCK_FILE = ".lock_file" + SYNC_LOCK_FILE = ".sync_lock_file" - def initialize (name, location, server_url, log) + def initialize( name, location, server_url, pkg_server ) @name = name @location = location - @pkg_hash_os = {} - @log = log @server_url = server_url + @log = pkg_server.log + @integrity = pkg_server.integrity + @lock_file_path = "#{location}/#{LOCK_FILE}" + @sync_lock_file_path = "#{location}/#{SYNC_LOCK_FILE}" + @pkg_hash_os = {} + @archive_pkg_list = [] + @snapshot_hash = [] + @support_os_list = [] + @last_sync_changes = "" + @log.info "Distribution class[#{name}] initialize " - for os in SUPPORTED_OS - if @location.empty? or ( not File.exist? "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) then - @pkg_hash_os[os] = {} - else - @pkg_hash_os[os] = Parser.read_pkg_list( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - end - end + initialize_pkg_list() end - def register (file_path, pkg) - @log.info "Distribution class's register" + def register (file_path, pkg, internal_flag) if pkg.nil? then raise RuntimeError, "package file does not contain pkginfo.manifest: [#{file_path}]" end + if not @pkg_hash_os.has_key?(pkg.os) then + raise RuntimeError, "package server does not support package's os : [#{pkg.os}]" + end + exist_pkg = @pkg_hash_os[pkg.os][pkg.package_name] - + # version check and if existing version is higher then upload version? - if not exist_pkg.nil? - if not ( Utils.compare_version( exist_pkg.version, pkg.version ).eql? 1 ) then - raise RuntimeError, "existing package's version is higher than register package" + if (not exist_pkg.nil?) and (not internal_flag) then + if Utils.compare_version( exist_pkg.version, pkg.version ) != 1 then + raise RuntimeError, "existing package's version is higher then register package : [#{pkg.package_name}] in [#{pkg.os}]" end end # modified pkg class - pkg.origin = "local" - pkg.source = "" + pkg.origin = "local" + pkg.source = "" pkg.path = "/binary/" + File.basename( file_path ) - # TODO: windows and mac : sha256sum - if Utils::HOST_OS.eql? "linux" then - pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] - end - pkg.size = `du -b #{file_path}`.split[0].strip + if pkg.checksum.empty? then + # TODO: windows and mac : sha256sum + if Utils.is_unix_like_os( Utils::HOST_OS ) then + pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] + end + end + + if pkg.size.empty? then + pkg.size = `du -b #{file_path}`.split[0].strip + end + + @pkg_hash_os[pkg.os][pkg.package_name] = pkg - @pkg_hash_os[pkg.os][pkg.package_name] = pkg - return pkg end def register_for_test (file_path, pkg) - @log.info "Distribution class's register for test" if pkg.nil? then raise RuntimeError, "package file does not contain pkginfo.manifest: [#{file_path}]" end # modified pkg class - pkg.origin = "local" - pkg.source = "" + pkg.origin = "local" + pkg.source = "" pkg.path = "/temp/" + File.basename( file_path ) # TODO: windows and mac : sha256sum - if Utils::HOST_OS.eql? "linux" then + if Utils.is_unix_like_os( Utils::HOST_OS ) then pkg.checksum = `sha256sum #{file_path}`.split(" ")[0] end pkg.size = `du -b #{file_path}`.split[0].strip @@ -105,393 +118,837 @@ class Distribution return pkg end - def generate_snapshot (name, base_snapshot, append_pkg_list) - @log.info "Distribution class's generate snapshot" + def register_archive_pkg( archive_pkg ) + if not @archive_pkg_list.include? archive_pkg then + @archive_pkg_list.push archive_pkg + else + @log.error("archive package already exist : [#{archive_pkg}]", Log::LV_USER) + end + end + + def generate_snapshot(name, base_snapshot, from_cmd, change_log_string) # if name is nil or empty then create uniq name if name.nil? or name.empty? then name = Utils.create_uniq_name end - # check base snapshot exist - if File.exist? "#{@location}/snapshots/#{name}" then + # check base snapshot exist + if File.exist? "#{@location}/snapshots/#{name}" then raise "Snapshot is already exist: #{name}" end - if base_snapshot.nil? then base_snapshot = "" else base_snapshot.strip! end - if append_pkg_list.nil? then append_pkg_list = [] end + FileUtils.mkdir "#{@location}/snapshots/#{name}" + FileUtils.mkdir "#{@location}/changes" if not File.exists? "#{@location}/changes" + File.open( "#{@location}/changes/#{name}.log","w") { |f| f.puts change_log_string } + + # base_snapshot_path + if base_snapshot.empty? then + snapshot_path = @location + else + snapshot_path = "#{@location}/snapshots/#{base_snapshot.strip}" + end - if base_snapshot.empty? and append_pkg_list.empty? then - FileUtils.mkdir "#{@location}/snapshots/#{name}" + # copy package list + @support_os_list.each do |os| + FileUtils.copy_file( "#{snapshot_path}/#{PKG_LIST_FILE_PREFIX}#{os}", + "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}" ) + end - for os in SUPPORTED_OS - FileUtils.copy( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", - "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - end + # copy archive package list + FileUtils.copy_file( "#{snapshot_path}/#{ARCHIVE_PKG_FILE}", + "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_FILE}" ) - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" - end + # copy os info file + FileUtils.copy_file( "#{snapshot_path}/#{OS_INFO_FILE}", + "#{@location}/snapshots/#{name}/#{OS_INFO_FILE}" ) + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) - # base_snapshot is exist - elsif not ( base_snapshot.empty? ) then - FileUtils.mkdir "#{@location}/snapshots/#{name}" - - for os in SUPPORTED_OS - # check base snapshot exist - if (not File.exist? "#{@location}/snapshots/#{base_snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}") then - raise RuntimeError, "Can't find base snapshot [#{base_snapshot}]" - end - - base_pkg_list = Parser.read_pkg_list( "#{@location}/snapshots/#{base_snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - snapshot_generate2( name, os, base_pkg_list, append_pkg_list ) + if File.exist? tmp_file_name then + tmp_file_name = "" end - - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" + end + + FileUtils.copy_file( "#{@location}/#{SNAPSHOT_INFO_FILE}", tmp_file_name ) + File.open( tmp_file_name, "a" ) do |f| + f.puts "name : #{name}" + f.puts "time : #{Time.now.strftime("%Y%m%d%H%M%S")}" + if from_cmd then + f.puts "type : manual" + else + f.puts "type : auto" end + f.puts "path : /snapshots/#{name}" + f.puts + end + FileUtils.mv( tmp_file_name, "#{@location}/#{SNAPSHOT_INFO_FILE}", :force => true ) + + # snapshot is generated + @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) + return name + end - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) - # base_snapshot is empty - else - FileUtils.mkdir "#{@location}/snapshots/#{name}" + def sync(force, snapshot = "") + pkg_list_update_flag = false + archive_update_flag = false + distribution_update_flag = false + changes = [] + + # lock + sync_lock_file = Utils.file_lock(@sync_lock_file_path) - for os in SUPPORTED_OS - base_pkg_list = Parser.read_pkg_list( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) - snapshot_generate2( name, os, base_pkg_list, append_pkg_list ) - end + # reload pkg list from newest pkg list file + reload_distribution_information() - # copy archive package list - begin - FileUtils.copy( "#{@location}/#{ARCHIVE_PKG_LIST}", "#{@location}/snapshots/#{name}/#{ARCHIVE_PKG_LIST}" ) - rescue => e - @log.warn "ARCHIVE_PKG_LIST not exist" - end + # check distribution's server_url + if @server_url.empty? then + @log.error("This distribution has not remote server") + Utils.file_unlock(sync_lock_file) + raise RuntimeError, "remote server address empty" + end - @log.output( "snapshot is generated : #{@location}/snapshots/#{name}", Log::LV_USER) + # generate client class + if snapshot.nil? or snapshot.empty? then + server_url = @server_url + else + server_url = "#{@server_url}/snapshots/#{snapshot}" end - end + client = Client.new( server_url, "#{@location}/binary", @log ) - def snapshot_generate2( name, os, pkg_list, append_pkg_list ) - @log.info "snapshot_generate2: input append_pkg_list #{append_pkg_list}" - append_pkg_list.each do |pkg| - # os check - if pkg.os.eql? os.strip then pkg_list[pkg.package_name] = pkg end + # parents package server check + if client.pkg_hash_os.keys.empty? then + @log.error("Sync process stopped by error.") + @log.error("Parents package server does not have [[os_info]] file.") + + Utils.file_unlock(sync_lock_file) + raise RuntimeError, "Parents package server does not have [[os_info]] file." end - - File.open( "#{@location}/snapshots/#{name}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| - pkg_list.each_value do |pkg| - pkg.print_to_file(f) - f.puts + + # update os list + add_os_list = client.support_os_list - @support_os_list + add_os_list.each do |os| + add_os(os) + changes.push "Add OS #{os}" + pkg_list_update_flag = true + end + + if force then + remove_os_list = @support_os_list - client.support_os_list + remove_os_list.each do |os| + remove_os(os) + changes.push "Remove OS #{os}" + pkg_list_update_flag = true end - end - end + end + update_pkg_list = [] - def sync( force, os ) + @support_os_list.each do |os| + # error check + if client.pkg_hash_os[os].nil? then + @log.error("os[[#{os}]] is removed in parents package server", Log::LV_USER) + next + end - # check distribution's server_url - if @server_url.empty? then - @log.error( "This distribution has not remote server" , Log::LV_USER) - return + server_pkg_name_list = client.pkg_hash_os[os].keys + local_pkg_name_list = @pkg_hash_os[os].keys + full_pkg_name_list = server_pkg_name_list + local_pkg_name_list + full_pkg_name_list.uniq! + + full_pkg_name_list.each do |pkg_name| + ret = sync_package( pkg_name, client, os, force ) + if not ret.nil? then + update_pkg_list.push(ret) + pkg_list_update_flag = true + end + end end - # generate client class - client_bin = Client.new( @server_url, "#{@location}/binary", @log ) - client_bin.update - client_src = Client.new( @server_url, "#{@location}/source", @log ) - client_src.update - - source_pkg_path_list = [] - dep_pkg_path_list = [] - - # error check - if client_bin.pkg_hash_os[os].nil? then - raise "Package list can't generated. url is #{@server_url}. os is #{os}" - end - - # check existing source package list - @pkg_hash_os[os].each_value do |pkg| - if not source_pkg_path_list.include? pkg.src_path then - source_pkg_path_list.push pkg.src_path - end - end - - full_pkg_list = client_bin.pkg_hash_os[os].merge(@pkg_hash_os[os]) - - full_pkg_list.each_key do |pkg_name| - server_pkg = client_bin.pkg_hash_os[os][pkg_name] - local_pkg = @pkg_hash_os[os][pkg_name] - - # if server and local has package - if ( not server_pkg.nil? ) and ( not local_pkg.nil? ) then - # if server version is not updated then skip - if not ( Utils.compare_version( local_pkg.version, server_pkg.version ).eql? 1 ) then - @log.info "existing packages version equal or higher then server's version so package[#{pkg_name}] skip" - @log.info "server package version: [#{server_pkg.version}]" - @log.info "local package version: [#{local_pkg.version}]" - - next - end + # sync archive package + update_archive_list = sync_archive_pkg() - # if server version is not updated then skip - # if server's pakcage is local package and mode is not force then local package will be upaded - if ( local_pkg.origin.eql? "local" ) and ( not force ) then - @log.info "package [#{pkg_name}] is local package. so skip update" - - next - end - - # package update - @log.info "update package from server: [#{pkg_name}]" - file_path_list = client_bin.download( pkg_name, os, false ) - - # file download error check - if file_path_list.nil? or file_path_list.empty? then - @log.error( "Can't download package file #{pkg_name}" , Log::LV_USER) - next - else - @log.info "download binary package successfully: [#{pkg_name}]" - file_path = file_path_list[0] - end - - # update pkg class - server_pkg.path = "/binary/#{File.basename(file_path)}" - server_pkg.origin = client_bin.server_addr - @pkg_hash_os[os][pkg_name] = server_pkg - - dep_pkg_path_list = dep_pkg_path_list + server_pkg.source_dep_list - - # if binary only package, then skip downloading its source - if server_pkg.src_path.empty? then next end - - # if binary's source package is not downlaoded, download it - if ( not source_pkg_path_list.include? server_pkg.src_path ) then - @log.info "download source package: [#{server_pkg.src_path}]" - file = client_src.download_source( pkg_name, os ) - if file.nil? then - @log.error "Can't download source package [#{pkg_name}]" + # lock + lock_file = Utils.file_lock(@lock_file_path) + + # reload pkg list from newest pkg list file + reload_distribution_information() + + # update pkg_list hash + update_pkg_list.each do |update_option, os, pkg| + # if updated package's os is removed then skip update + if not @support_os_list.include? os then + next + end + + case update_option + when "ADD" + local_pkg = @pkg_hash_os[os][pkg.package_name] + + if (not force) and (not local_pkg.nil?) then + # if updated package 'local' package then skip + if local_pkg.origin.eql? "local" then + next + end + + # if package is update when sync time then skip + if Utils.compare_version(local_pkg.version, pkg.version) == -1 then + next else - source_pkg_path_list.push server_pkg.src_path - end - end - # if package exist only server - elsif ( not server_pkg.nil? ) then - #downnlaod binary package - file_path_list = client_bin.download( pkg_name, os, false ) - - # file download error check - if file_path_list.nil? or file_path_list.empty? then - @log.error( "Can't download package file #{pkg_name}", Log::LV_USER) - next - else - @log.info "download binary package successfully: [#{pkg_name}]" - file_path = file_path_list[0] - end - - # update pkg class - server_pkg.path = "/binary/#{File.basename(file_path)}" - server_pkg.origin = client_bin.server_addr - @pkg_hash_os[os][pkg_name] = server_pkg - - dep_pkg_path_list = dep_pkg_path_list + server_pkg.source_dep_list - - # if binary only package, then skip downloading its source - if server_pkg.src_path.empty? then next end - - # if binary's source package is not downlaoded, download it - if not source_pkg_path_list.include? server_pkg.src_path then - @log.info "download source package: [#{server_pkg.src_path}]" - file = client_src.download_source( pkg_name, os ) - if file.nil? - @log.error "Can't download source package [#{server_pkg.src_path}]" - else - source_pkg_path_list.push server_pkg.src_path + @log.info( "update package [#{pkg.package_name}] in #{pkg.os}", Log::LV_USER) end end - # if package exist only local - elsif ( not local_pkg.nil? ) then - # if pakcage is not local package then server's package is removed - # so, local package remove - if not local_pkg.origin.eql? "local" then - @pkg_hash_os[os].delete(pkg_name) + + @pkg_hash_os[os][pkg.package_name] = pkg + changes.push "Package: #{pkg.package_name} changes: #{pkg.get_changes}" if pkg.does_change_exist? + when "REMOVE" + if not force then + if @pkg_hash_os[os][pkg.package_name].origin.eql? "local" then + else + @log.info( "remove package [#{pkg.package_name}] in #{pkg.os}", Log::LV_USER) + next + end end + + @pkg_hash_os[os].delete(pkg.package_name) + changes.push "#{pkg.package_name} #{os} removed" else - raise RuntimeError,"hash merge error!" + @log.error("Unsupportd update option : #{update_option}", Log::LV_USER) + next + end + end + + update_archive_list.each do |pkg| + if not @archive_pkg_list.include? pkg then + @archive_pkg_list.push pkg + changes.push "Add archive package #{pkg}" + archive_update_flag = true end end - @log.info "pkg file update end" - # download dependency source packages - dep_pkg_path_list.uniq.each do |dep| - if dep.package_name.strip.empty? then next end - @log.info "download dep package: [#{dep.package_name}]" - file = client_src.download_dep_source( dep.package_name ) - if file.nil? - @log.error "Can't download dep package [#{dep.package_name}]" - end - end + # update pkg_list file + if pkg_list_update_flag then + write_all_pkg_list() + distribution_update_flag = true + end + + # update archive list file + if archive_update_flag then + write_archive_pkg_list() + distribution_update_flag = true + end + + # unlock + Utils.file_unlock(lock_file) + Utils.file_unlock(sync_lock_file) + + if not changes.empty? then + @last_sync_changes = "SYSTEM: sync parents server \n#{changes.uniq.join("\n\n")}" + end - @log.info "pkg deb file update end" - # pakcage list file update - write_pkg_list(os) - @log.info "write pkg list" + return distribution_update_flag end - def sync_archive_pkg - client = Client.new( @server_url, "#{@location}/source", @log ) - client.update - - downloaded_list = [] - client.archive_pkg_list.each do |pkg| - if not File.exist? "#{@location}/source/#{pkg}" then - file = client.download_dep_source(pkg) - if file.nil? - @log.error "Can't download archive package [#{file}]" - else - downloaded_list.push pkg + def add_os(os) + if @support_os_list.include? os then + @log.error("#{os} is already exist ", Log::LV_USER) + return + end + + # update os information + @support_os_list.push os + @pkg_hash_os[os] = {} + File.open("#{@location}/#{OS_INFO_FILE}", "a") do |f| + f.puts os + end + + # create pkg_list_#{os} file + File.open( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| end + end + + def clean( remain_snapshot_list ) + file_list = [] + used_archive_list = [] + + # collect remaining file's name from current package server version + @support_os_list.each do |os| + @pkg_hash_os[os].each_value do |pkg| + file_list.push(pkg.path.sub("/binary/","")) + + pkg.source_dep_list.each do |source_dep| + if @archive_pkg_list.include? source_dep.package_name then + used_archive_list.push source_dep.package_name + else + @log.error("Can't find dependency source package : #{source_dep.package_name}") + end end end - end + end + + # remain only used archive package + @archive_pkg_list = used_archive_list.uniq + write_archive_pkg_list + + # collect remaning file's name from snapshot list + remain_snapshot_list.each do |snapshot| + os_info = "#{@location}/snapshots/#{snapshot}/#{OS_INFO_FILE}" + os_list = [] + # if snapshot has os_info file then using that file + if File.exist? os_info + File.open( os_info, "r" ) do |f| + f.each_line do |l| + os_list.push l.strip + end + end + # if snapshot does not have os_info file then using package server os_info list + else + os_list = @support_os_list + end + + os_list.each do |os| + begin + info_file = "#{@location}/snapshots/#{snapshot}/#{PKG_LIST_FILE_PREFIX}#{os}" + if not File.exist? info_file then + @log.error( "pkg list file does not exist : #{info_file}", Log::LV_USER) + next + end + + pkg_list = Parser.read_repo_pkg_list_from(info_file) + + pkg_list.each_value do |pkg| + file_list.push(pkg.path.sub("/binary/","")) + end + rescue => e + @log.error( e.message, Log::LV_USER) + end + end + + used_archive_list = used_archive_list + read_archive_pkg_list( snapshot ) + end + + file_list.uniq! + used_archive_list.uniq! + + # remove unused binary file + Dir.new( @location + "/binary" ).each do |file| + if file.start_with? "." then next end + + if not file_list.include? file then + FileUtils.rm "#{@location}/binary/#{file}" + end + end + + # remove unused archive file + Dir.new( @location + "/source" ).each do |file| + if file.start_with? "." then next end + + if not used_archive_list.include? file then + FileUtils.rm "#{@location}/source/#{file}" + end + end + + # remove unused snapshot + Dir.new( @location + "/snapshots" ).each do |snapshot| + if snapshot.start_with? "." then next end + + if not remain_snapshot_list.include? snapshot then + FileUtils.rm_rf "#{@location}/snapshots/#{snapshot}" + end + end + + # upate snapshot.info file + update_snapshot_info_file(remain_snapshot_list) + end - write_archive_pkg_list( downloaded_list ) - end + def write_all_pkg_list + @support_os_list.each do |os| + write_pkg_list(os) + end + end def write_pkg_list( os ) - File.open( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", "w" ) do |f| + # if input os is empty then return + if os.nil? or os.empty? then return end + + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + File.open( tmp_file_name, "w" ) do |f| @pkg_hash_os[os].each_value do |pkg| + # insert package information to file pkg.print_to_file(f) - f.puts + # insert empty line to file + f.puts end - end - end + end - def write_archive_pkg_list( pkg_file_name_list ) - File.open( "#{@location}/#{ARCHIVE_PKG_LIST}", "a" ) do |f| - pkg_file_name_list.map { |name| f.puts(name) } - end + FileUtils.mv( tmp_file_name, "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}", :force => true ) end - # input: package file path(zip file) - # return: pkg + # input: package file path(zip file) + # return: pkg def get_package_from_file(file_path) - tmp_dir = "./" + Utils.create_uniq_name - FileUtils.mkdir "#{@location}/#{tmp_dir}" - - # file extention is zip - if file_path.end_with? ".zip" then - system("unzip -q #{file_path} pkginfo.manifest -d #{@location}/#{tmp_dir} ") - # file extention is tar.gz - elsif file_path.end_with? ".tar.gz" or file_path.end_with? ".tar" then - system("tar -xzf #{file_path} -C #{@location}/#{tmp_dir}") + tmp_dir = @location + "/" + Utils.create_uniq_name + + #if file extension is .zip then check pkginfo.manifest + if File.extname(file_path).eql? ".zip" then + FileUtils.mkdir tmp_dir + + ret = FileInstaller.extract_a_file(file_path, "pkginfo.manifest", tmp_dir, @log) else - raise "unsupported zipping file. just use [zip/tar.gz]" + return nil end - pkg = Parser.read_pkginfo( "#{@location}/#{tmp_dir}/pkginfo.manifest" ) - FileUtils.rm_rf "#{@location}/#{tmp_dir}" - return pkg - end + # if pkginfo.manifest file exist + if not ret.nil? then + begin + pkg = Parser.read_single_pkginfo_from "#{tmp_dir}/pkginfo.manifest" + rescue => e + @log.error( e.message, Log::LV_USER) + return nil + end + + FileUtils.rm_rf tmp_dir + return pkg + # if pkginfo.manifest file does not exist + else + FileUtils.rm_rf tmp_dir + return nil + end + end + + def remove_pkg( pkg_name_list, os ) + if os.eql? "all" then os_list = @support_os_list + else os_list = [ os ] + end - def remove_pkg( pkg_name_list ) - for package_name in pkg_name_list - removed_flag = false + pkg_name_list.each do |package_name| + removed_flag = false - for os in SUPPORTED_OS - if @pkg_hash_os[os].key?(package_name) then - @log.info( "remove package [#{package_name}] in #{os}", Log::LV_USER) - @pkg_hash_os[os].delete(package_name) + os_list.each do |o| + if not @support_os_list.include? o then + @log.error( "package server does not support input os : #{o}") + next + end + + if @pkg_hash_os[o].key?(package_name) then + @log.info( "remove package [#{package_name}] in #{o}", Log::LV_USER) + @pkg_hash_os[o].delete(package_name) removed_flag = true end - end + end - if not removed_flag then - @log.error( "Can't find package: #{package_name}", Log::LV_USER) + if not removed_flag then + if @archive_pkg_list.include? package_name then + @archive_pkg_list.delete package_name + else + @log.error( "Can't find package: [#{package_name}]", Log::LV_USER) + end end - end + end # check install dependency integrity - check_instll_dependency_integrity + if @integrity.eql? "YES" then + @log.info "integrity check" + check_integrity + else + @log.info "skip integrity check" + end + - for os in SUPPORTED_OS - write_pkg_list(os) + # update pkg_list file + os_list.each do |o| + write_pkg_list(o) end - end + write_archive_pkg_list + end - def check_instll_dependency_integrity - @log.info "check server pkg's install dependency integrity" + def remove_snapshot( snapshot_list ) + remain_snapshot = [] + removed_snapshot = [] - for os in SUPPORTED_OS - for pkg in @pkg_hash_os[os].each_value - error_msg = "[#{pkg.package_name}]'s install dependency not matched in " + # remove unused snapshot + Dir.new( @location + "/snapshots" ).each do |snapshot| + if snapshot.start_with? "." then next end - for dep in pkg.install_dep_list - if @pkg_hash_os[os].has_key? dep.package_name then - target_pkg = @pkg_hash_os[os][dep.package_name] - else - raise RuntimeError,(error_msg + dep.to_s) - end - - # TODO: check just install dependency exist - next - - # check package's version - if not dep.match? target_pkg.version then - raise RuntimeError,(error_msg + dep.to_s) - end - - # TODO: install dependency's os is always ture - #if not dep.target_os_list.length == 0 then - # if not dep.target_os_list.include? target_pkg.os then - # raise RuntimeError,(error_msg + dep.to_s) - # end - #end - end - - # TODO: check just install dependency - next - - error_msg = "[#{pkg.package_name}]'s build dependency not matched in " - for dep in pkg.build_dep_list - if dep.target_os_list.length == 0 then - build_dep_os = os + if snapshot_list.include? snapshot then + FileUtils.rm_rf "#{@location}/snapshots/#{snapshot}" + snapshot_list.delete snapshot + removed_snapshot.push snapshot + else + remain_snapshot.push snapshot + end + end + + if not snapshot_list.empty? then + @log.output( "snapshot not exist : #{snapshot_list.join(",")}", Log::LV_USER ) + end + + if not removed_snapshot.empty? then + @log.output( "snapshot removed: #{removed_snapshot.join(",")}", Log::LV_USER ) + end + + update_snapshot_info_file(remain_snapshot) + end + + def check_integrity + @log.info "check server pkg's install dependency integrity" + + @support_os_list.each do |os| + @pkg_hash_os[os].each_value.each do |pkg| + check_package_integrity(pkg) + end + end + end + + def check_package_integrity(pkg) + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s install dependency not matched in " + os = pkg.os + + pkg.install_dep_list.each do |dep| + if @pkg_hash_os[os].has_key? dep.package_name then + target_pkg = @pkg_hash_os[os][dep.package_name] + else + raise RuntimeError,(error_msg + dep.to_s) + end + + # check package's version + if not dep.match? target_pkg.version then + raise RuntimeError,(error_msg + dep.to_s) + end + + end + + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s build dependency not matched in " + pkg.build_dep_list.each do |dep| + if dep.target_os_list.length == 0 then + build_dep_os = os + else + build_dep_os = dep.target_os_list[0] + end + + if @pkg_hash_os[build_dep_os].has_key? dep.package_name then + target_pkg = @pkg_hash_os[build_dep_os][dep.package_name] + else + raise RuntimeError,(error_msg + dep.to_s) + end + + # check package's version + if not dep.match? target_pkg.version then + raise RuntimeError,(error_msg + dep.to_s) + end + end + + error_msg = "[[#{pkg.package_name}] in #{pkg.os}]'s source dependency not matched in " + pkg.source_dep_list.each do |dep| + if not @archive_pkg_list.include? dep.package_name then + raise RuntimeError,(error_msg + dep.to_s) + end + end + end + + def read_archive_pkg_list( snapshot_name ) + pkg_list = [] + + if snapshot_name.empty? + file_name = @location + "/" + ARCHIVE_PKG_FILE + else + file_name = @location + "/snapshots/" + snapshot_name + "/" + ARCHIVE_PKG_FILE + end + + if File.exist? file_name + File.open(file_name, "r") do |f| + f.each_line do |l| + pkg_list.push(l.strip) + end + end + end + + return pkg_list + end + + def write_archive_pkg_list() + File.open( "#{@location}/#{ARCHIVE_PKG_FILE}", "w" ) do |f| + @archive_pkg_list.each do |pkg| + f.puts(pkg) + end + end + end + + def initialize_pkg_list + if not File.exist? "#{@location}/#{OS_INFO_FILE}" then + return + end + + # get support_os_list + @support_os_list = [] + File.open( "#{@location}/#{OS_INFO_FILE}", "r" ) do |f| + f.each_line do |l| + @support_os_list.push l.strip + end + end + + # read package_list file + @support_os_list.each do |os| + @pkg_hash_os[os] = {} + pkg_list_file = "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" + + if File.exist? pkg_list_file then + begin + @pkg_hash_os[os] = Parser.read_repo_pkg_list_from( pkg_list_file ) + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = nil + end + end + end + + # read archive package_list file + @archive_pkg_list = read_archive_pkg_list("") + end + + def get_link_package(pkg, pkg_os) + pkg.os_list.each do |os| + # skip in same os for origin package + if pkg_os.eql? os then next end + # skip in unsupported os + if not @support_os_list.include? os then next end + + exist_pkg = @pkg_hash_os[os][pkg.package_name] + if exist_pkg.nil? then next end + + compare_version = Utils.compare_version(pkg.version, exist_pkg.version) + # if version same then compatible package + if compare_version == 0 then + return exist_pkg + end + end + + return nil + end + + # PRIVATE METHODS/VARIABLES + private + + def sync_package( pkg_name, client, os, force ) + server_pkg = client.pkg_hash_os[os][pkg_name] + local_pkg = @pkg_hash_os[os][pkg_name] + + # if server and local has package + if ( not server_pkg.nil? ) and ( not local_pkg.nil? ) then + version_cmp = Utils.compare_version( local_pkg.version, server_pkg.version ) + if ( version_cmp == 0 ) then + # version is same then skip update + return nil + end + + if ( version_cmp == -1 ) then + # local package's version is higher than server packages's version then skip update + return nil + end + + if ( local_pkg.origin.eql? "local" ) and (not force) then + # local_pkg is generated from local and not force mode then skip update + return nil + end + + pkg = sync_package2( server_pkg, client, os ) + return ["ADD", os, pkg] + # if package exist only server + elsif ( not server_pkg.nil? ) then + pkg = sync_package2( server_pkg, client, os ) + return ["ADD", os, pkg] + # if package exist only local + elsif ( not local_pkg.nil? ) then + # if local pkg is generated from local then skip + if local_pkg.origin.eql? "local" and (not force) then + return nil + end + + # package remove + return ["REMOVE", os, local_pkg] + else + raise RuntimeError,"hash merge error!" + end + + return nil + end + + def sync_package2( pkg, client, os ) + pkg_name = pkg.package_name + + # package update + file_path_list = client.download( pkg_name, os, false ) + + # file download error check + if file_path_list.nil? or file_path_list.empty? then + @log.error("Can't download package file [#{pkg_name}]", Log::LV_USER) + return nil + else + file_path = file_path_list[0] + end + + # update pkg class + pkg.path = "/binary/#{File.basename(file_path)}" + pkg.origin = client.server_addr + return pkg + + end + + def update_snapshot_info_file(remain_snapshot_list) + if not File.exist? "#{@location}/#{SNAPSHOT_INFO_FILE}" + @log.error "Can not find snapshot info file" + return + end + + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + # modify snapshot info File + info_file = File.readlines("#{@location}/#{SNAPSHOT_INFO_FILE}") + File.open(tmp_file_name, 'w') do |f| + save_flag = false + info_file.each do |line| + if line =~ /name :/ then + if remain_snapshot_list.include? line.split(':')[1].strip then + save_flag = true else - build_dep_os = dep.target_os_list[0] + save_flag = false end - if @pkg_hash_os[build_dep_os].has_key? dep.package_name then - target_pkg = @pkg_hash_os[build_dep_os][dep.package_name] - else - raise RuntimeError,(error_msg + dep.to_s) - end - - # check package's version - if not dep.match? target_pkg.version then - raise RuntimeError,(error_msg + dep.to_s) - end - - # TODO: check package's target_os - #if not dep.target_os_list.length == 0 then - # if not dep.target_os_list.include? target_pkg.os then - # raise RuntimeError,(error_msg + dep.to_s) - # end - #end - end - - error_msg = "[#{pkg.package_name}]'s source dependency not matched in " - for dep in pkg.source_dep_list - # check source package exist - if not File.exist? "#{@location}/source/#{dep.package_name}" - raise RuntimeError,(error_msg + dep.to_s) - end - end - end - end + end + + if save_flag then + f.puts line + end + end + end + + FileUtils.mv( tmp_file_name, "#{@location}/#{SNAPSHOT_INFO_FILE}", :force => true ) + end + + def get_all_reverse_depends_pkgs(pkg, checked_list) + depends_list = [] + + @support_os_list.each do |os| + @pkg_hash_os[os].each_value do |dpkg| + if dpkg.install_dep_list.include? pkg or \ + dpkg.build_dep_list.include? pkg then + depends_list.push opkg + end + + end + end + + depends_list.each do |dpkg| + checked_list.push dpkg + rdepends_list = get_all_reverse_depends_pkgs( dpkg, checked_list ) + end + + return rdepends_list + end + + def reload_distribution_information + if not File.exist?("#{@location}/#{OS_INFO_FILE}") then + return + end + + # get support_os_list + @support_os_list = [] + File.open( "#{@location}/#{OS_INFO_FILE}", "r" ) do |f| + f.each_line do |l| + @support_os_list.push l.strip + end + end + + # read binary package_list file + @support_os_list.each do |os| + @pkg_hash_os[os] = {} + pkg_list_file = "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" + + if File.exist? pkg_list_file then + begin + @pkg_hash_os[os] = Parser.read_repo_pkg_list_from( pkg_list_file ) + rescue => e + @log.error( e.message, Log::LV_USER) + @pkg_hash_os[os] = nil + end + end + end + + # read archive package_list file + @archive_pkg_list = read_archive_pkg_list( "" ) + end + + def remove_os(os) + if not @support_os_list.include? os then + @log.error("Can't remove os : #{os} does not exist ", Log::LV_USER) + end + + # update os information + @support_os_list.delete os + @pkg_hash_os.delete os + + # generate temp file + tmp_file_name = "" + while ( tmp_file_name.empty? ) + tmp_file_name = @location + "/temp/." + Utils.create_uniq_name + + if File.exist? tmp_file_name then + tmp_file_name = "" + end + end + + info_file = File.readlines("#{@location}/#{OS_INFO_FILE}") + File.open(tmp_file_name, "w") do |f| + info_file.each do |line| + if not line.strip.eql? os then + f.puts line + end + end + end + + FileUtils.mv( tmp_file_name, "#{@location}/#{OS_INFO_FILE}", :force => true ) + + # delete pkg_list_#{os} file + File.delete( "#{@location}/#{PKG_LIST_FILE_PREFIX}#{os}" ) + end + + def sync_archive_pkg + client = Client.new( @server_url, "#{@location}/source", @log ) + + download_list = client.archive_pkg_list - @archive_pkg_list + + updated_file_list = [] + + # if update list is empty then return empty array + if download_list.empty? then return updated_file_list end + + download_list.each do |pkg| + file = client.download_dep_source(pkg) + if file.nil? + @log.error("Can't download archive package [#{pkg}]", Log::LV_USER) + else + updated_file_list.push pkg + end + end + + return updated_file_list end + end diff --git a/src/pkg_server/downloader.rb b/src/pkg_server/downloader.rb index 0308208..b5dd1ec 100644 --- a/src/pkg_server/downloader.rb +++ b/src/pkg_server/downloader.rb @@ -1,6 +1,6 @@ =begin - - downloader.rb + + downloader.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -31,38 +31,40 @@ require "utils" class FileDownLoader - @@log = nil - - def FileDownLoader.set_logger(logger) - @@log = logger - end - - def FileDownLoader.download(url, path) - ret = false - - if not File.directory? path then - @@log.error "\"#{path}\" does not exist" - return ret - end - - is_remote = Utils.is_url_remote(url) - filename = url.split('/')[-1] - - fullpath = File.join(path, filename) - - if is_remote then - ret = system "wget #{url} -O #{fullpath} -nv" - else - if not File.exist? url then - @@log.error "\"#{url}\" file does not exist" - return false - else - ret = system "cp #{url} #{fullpath}" - end - end - - # need verify - return ret - end + def FileDownLoader.download(url, path, logger) + ret = false + + if not File.directory? path then + logger.error "\"#{path}\" does not exist" + return ret + end + + is_remote = Utils.is_url_remote(url) + filename = url.split('/')[-1] + fullpath = File.join(path, filename) + + logger.info "Downloading #{url}" + if is_remote then + pid,status = Utils.execute_shell_with_log( "wget #{url} -O #{fullpath} -nv --tries=3", logger.path ) + ret = (not status.nil? and status.exitstatus != 0) ? false : true + #ret = Utils.execute_shell( "wget #{url} -O #{fullpath} -q") + else + if not File.exist? url then + logger.error "\"#{url}\" file does not exist" + return false + else + ret = system "cp #{url} #{fullpath}" + end + end + + # need verify + if ret then + logger.info "Downloaded #{filename}.. OK" + else + logger.info "Failed to download #{filename}" + logger.info " [dist: #{path}]" + end + return ret + end end diff --git a/src/pkg_server/installer.rb b/src/pkg_server/installer.rb index 0006b8d..58febe9 100644 --- a/src/pkg_server/installer.rb +++ b/src/pkg_server/installer.rb @@ -1,6 +1,6 @@ =begin - - installer.rb + + installer.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -28,273 +28,462 @@ Contributors: $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" -require "serverConfig" +require "packageServerConfig" require "log" require "utils" +if Utils.is_windows_like_os( Utils::HOST_OS ) then + require "rubygems" + require "zip/zip" +end class FileInstaller - CONFIG_PATH = "#{$build_tools}/client" - PACKAGE_INFO_DIR = ".info" - - @@log = nil - - def FileInstaller.set_logger(logger) - @@log = logger - end - - def FileInstaller.install(package_name, package_file_path, type, target_path) - - if not File.exist? package_file_path then - @@log.error "\"#{package_file_path}\" file does not exist." - return false - end - - case type - # install script when binary package - when "binary" then - uniq_name = Utils.create_uniq_name - path = Utils::HOME + "/tmp/#{uniq_name}" - if Utils::HOST_OS.eql? "windows" then - drive = Utils::HOME.split("/")[0] - path = "#{drive}/#{uniq_name}" - end - FileUtils.mkdir_p "#{path}" - - if File.directory? path then - log = "##### create temporary dir : #{path} #####\n" - else - log = "##### [Failed] create temporary dir : #{path} #####\n" - return false - end - - log = log + "##### extract file : #{package_file_path} #####\n" - log = log + extract_file(package_name, package_file_path, path, target_path) - move_dir(package_name, path, target_path) - - log = log + "##### execute install script #####\n" - log = log + execute_install_script(package_name, path, target_path) - - log = log + "##### move remove script #####\n" - move_remove_script(package_name, path, target_path) - - log = log + "##### remove temporary dir : #{path} #####\n" - Utils.execute_shell("rm -rf #{path}") - - target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_config_path) - pkg_inst_log = "#{package_name}_inst.log" - pkg_inst_log_path = File.join(target_config_path, pkg_inst_log) - - File.open(pkg_inst_log_path, "a+") do |f| - f.puts log - end - - when "source" then - end - - # need verify - return true; - end - - def FileInstaller.move_remove_script(package_name, path, target_path) - target_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_path) - script_file_prefix = "#{path}/remove.*" - script_file = Dir.glob(script_file_prefix)[0] - - if not script_file.nil? then - FileUtils.mv(script_file, target_path) - end - end - - - def FileInstaller.execute_install_script(package_name, path, target_path) - script_file_prefix = "#{path}/install.*" - script_file = Dir.glob(script_file_prefix)[0] - log = "" - - if not script_file.nil? then - @@log.info "Execute \"#{script_file}\" file" - cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" - log = `#{cmd}` - end - return log - end - - def FileInstaller.execute_remove_script(package_name, target_path) - info_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - if not File.directory? info_path then - return false - end - - script_file_prefix = "#{info_path}/remove.*" - script_file = Dir.glob(script_file_prefix)[0] - log = "" - - if not script_file.nil? then - @@log.info "Execute \"#{script_file}\" file" - cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" - log = `#{cmd}` - end - end - - def FileInstaller.remove_pkg_files(package_name, target_path) - list_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - - if not File.directory? list_path then - return false - end - - list_file_name = "#{list_path}/#{package_name}.list" - list_file = Dir.glob(list_file_name)[0] - directories = [] - - if not list_file.nil? then - File.open(list_file, "r") do |file| - file.each_line do |f| - f = f.strip - if f.nil? or f.empty? then next end - file_path = File.join(target_path, f) - if File.directory? file_path then - if File.symlink? file_path then - File.unlink file_path - next - end - entries = Dir.entries(file_path) - if entries.include? "." then entries.delete(".") end - if entries.include? ".." then entries.delete("..") end - if entries.empty? or entries.nil? then - begin - Dir.rmdir(file_path) - rescue SystemCallError - @@log.warn "\"#{file_path}\" directory is not empty" - end - else directories.push(file_path) end - elsif File.file? file_path then FileUtils.rm_f(file_path) - elsif File.symlink? file_path then File.unlink file_path - # if files are already removed by remove script, - else @@log.warn "\"#{file_path}\" does not exist" end - end - end - - directories.reverse.each do |path| - entries = Dir.entries(path) - if entries.include? "." then entries.delete(".") end - if entries.include? ".." then entries.delete("..") end - if entries.empty? or entries.nil? then - begin - Dir.rmdir(path) - rescue SystemCallError - @@log.warn "\"#{file_path}\" directory is not empty" - end - else next end - end - end - #FileUtils.rm_rf(list_path) - Utils.execute_shell("rm -rf #{list_path}") - return true - end - - def FileInstaller.uninstall(package_name, type, target_path) - case type - when "binary" then - execute_remove_script(package_name, target_path) - remove_pkg_files(package_name, target_path) - when "source" then - end - - return true - end - - def FileInstaller.move_dir(package_name, source_path, target_path) - config_path = File.join(target_path, PACKAGE_INFO_DIR, package_name) - FileUtils.cp_r Dir.glob("#{source_path}/data/*"), target_path - FileUtils.cp "#{source_path}/pkginfo.manifest", config_path - end - - def FileInstaller.extract_file(package_name, package_file_path, path, target_path) - dirname = File.dirname(package_file_path) - filename = File.basename(package_file_path) - ext = File.extname(filename) - - target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" - FileUtils.mkdir_p(target_config_path) - pkg_file_list = "#{package_name}.list" - pkg_file_list_path = File.join(target_config_path, pkg_file_list) - temp_pkg_file_list = "temp_file_list" - temp_pkg_file_list_path = File.join(target_config_path, "temp_file_list") - - show_file_list_command = nil - extrach_file_list_command = nil - - case ext - when ".zip" then - show_file_list_command = "zip -sf #{package_file_path}" - extract_file_list_command = "unzip \"#{package_file_path}\" -d \"#{path}\"" - when ".tar" then - show_file_list_command = "tar -sf #{package_file_path}" - extract_file_list_command = "tar xf \"#{package_file_path}\" -C \"#{path}\"" - else - @@log.error "\"#{filename}\" is not supported." - return nil - end - - system "#{show_file_list_command} > #{temp_pkg_file_list_path}" - File.open(pkg_file_list_path, "a+") do |targetfile| - File.open(temp_pkg_file_list_path, "r") do |sourcefile| - sourcefile.each_line do |l| - if l.strip.start_with? "data/" then - ml = l.strip[5..-1] - targetfile.puts ml - else next end - end - end - end - - File.delete(temp_pkg_file_list_path) - log = `#{extract_file_list_command}` - @@log.info "Extracted \"#{filename}\" file.." - if log.nil? then log = "" end - return log - end - - def FileInstaller.extract_specified_file(package_file_path, target_file, path) - dirname = File.dirname(package_file_path) - filename = File.basename(package_file_path) - ext = File.extname(filename) - - case ext - when ".zip" then - if not path.nil? then - extract_file_command = "unzip -x #{package_file_path} #{target_file} -d #{path}" - else - extract_file_command = "unzip -x #{package_file_path} #{target_file}" - end - when ".tar" then - if not path.nil? then - path = File.join(path, package_file_path) - extract_file_command = "tar xvf #{package_file_path} #{target_file}" - else - extract_file_command = "tar xvf #{package_file_path} #{target_file}" - end - end - - system "#{extract_file_command}" - - if not path.nil? then - target_file_path = File.join(path, target_file) - else - target_file_path = target_file - end - - if File.exist? target_file_path then - @@log.info "Extracted \"#{target_file}\" file.." - return true - else - @@log.info "Failed to extracted \"#{target_file}\" file.." - return false - end - end + CONFIG_PATH = "#{PackageServerConfig::CONFIG_ROOT}/client" + PACKAGE_INFO_DIR = ".info" + PACKAGE_MANIFEST = "pkginfo.manifest" + + def FileInstaller.install(package_name, package_file_path, type, target_path, logger) + + if not File.exist? package_file_path then + logger.error "\"#{package_file_path}\" file does not exist." + return false + end + + case type + # install script when binary package + when "binary" then + uniq_name = Utils.create_uniq_name + path = Utils::HOME + "/tmp/#{uniq_name}" + # windows has limitation for file path length + if Utils.is_windows_like_os( Utils::HOST_OS ) then + drive = Utils::HOME.split("/")[0] + path = "#{drive}/#{uniq_name}" + end + if not File.exist? path then FileUtils.mkdir_p "#{path}" end + + if File.directory? path then + log = "## create temporary dir : #{path}\n" + else + logger.error "Failed to create temporary dir" + logger.info " [path: #{path}]" + return false + end + + begin + logger.info "Installing \"#{package_name}\" package.." + logger.info " [file: #{package_file_path}]" + + log = log + "## Extract file : #{package_file_path}\n" + result = extract_file(package_name, package_file_path, path, target_path, logger) + if result == "" or result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Move files : \"#{path}\" to \"#{target_path}\"\n" + result = move_dir(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Execute install script\n" + result = execute_install_script(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Move remove script\n" + result = move_remove_script(package_name, path, target_path, logger) + if result.nil? then + write_log(target_path, package_name, log) + return false + else log = log + result end + + log = log + "## Remove temporary dir : #{path} #####\n" + result = Utils.execute_shell_return("rm -rf #{path}") + if result.nil? then + logger.warn "Failed to remove temporary path" + logger.info " [path: #{path}]" + end + rescue Interrupt + logger.error "FileInstaller: Interrupted.." + Utils.execute_shell("rm -rf #{path}") + logger.info "Removed #{path}" + raise Interrupt + end + write_log(target_path, package_name, log) +=begin + target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end + pkg_inst_log = "#{package_name}_inst.log" + pkg_inst_log_path = File.join(target_config_path, pkg_inst_log) + + File.open(pkg_inst_log_path, "a+") do |f| + f.puts log + end +=end + when "source" then + end + + # need verify + logger.info "Installed \"#{package_name}\" package.. OK" + logger.info " [path: #{target_path}]" + return true; + end + + def FileInstaller.write_log(target_path, package_name, log) + target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end + pkg_inst_log = "#{package_name}_inst.log" + pkg_inst_log_path = File.join(target_config_path, pkg_inst_log) + + File.open(pkg_inst_log_path, "a+") do |f| + f.puts log + end + end + + def FileInstaller.move_remove_script(package_name, path, target_path, logger) + target_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.exist? target_path then FileUtils.mkdir_p(target_path) end + script_file_prefix = "#{path}/remove.*" + script_file = Dir.glob(script_file_prefix)[0] + log = "" + + if not script_file.nil? then + result = Utils.execute_shell_return("mv #{script_file} #{target_path}") + if result.nil? then + logger.error "Failed to move a remove script" + logger.info " [file: #{script_file}]" + logger.info " [from: #{path}]" + logger.info " [to: #{target_path}]" + return nil + else log = result.join("") end + logger.info "Moved remove script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[from: #{path}]\n" + log = log + "[to: #{target_path}]\n" + end + + return log + end + + + # Does not verify that the script execution is successful. + # Register shortcut should be failed. + def FileInstaller.execute_install_script(package_name, path, target_path, logger) + script_file_prefix = "#{path}/install.*" + script_file = Dir.glob(script_file_prefix)[0] + log = "" + + if not script_file.nil? then + logger.info "Execute \"#{script_file}\" file" + if Utils.is_windows_like_os( Utils::HOST_OS ) then + target_path = target_path.gsub("/","\\") + cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" + else + `chmod +x #{script_file}` + cmd = "INSTALLED_PATH=\"#{target_path}\" #{script_file}" + end + logger.info " [cmd: #{cmd}]" + log = `#{cmd}` + logger.info "Executed install script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[cmd: #{cmd}]\n" + end + + return log + end + + # Does not verify that the script execution is successful. + # Removing shortcut should be failed. + def FileInstaller.execute_remove_script(package_name, target_path, logger) + info_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.directory? info_path then + logger.error "\"#{info_path}\" does not exist." + return nil + end + + script_file_prefix = "#{info_path}/remove.*" + script_file = Dir.glob(script_file_prefix)[0] + log = "" + + if not script_file.nil? then + logger.info "Execute \"#{script_file}\" file" + if Utils.is_windows_like_os( Utils::HOST_OS ) then + target_path = target_path.gsub("/","\\") + cmd = "set INSTALLED_PATH=\"#{target_path}\"& #{script_file}" + else + `chmod +x #{script_file}` + cmd = "INSTALLED_PATH=\"#{target_path}\" #{script_file}" + end + logger.info " [cmd: #{cmd}]" + log = `#{cmd}` + logger.info "Executed remote script file.. OK" + log = log + "[file: #{script_file}]\n" + log = log + "[cmd: #{cmd}]\n" + end + + return log + end + + def FileInstaller.remove_pkg_files(package_name, target_path, logger) + list_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + + if not File.directory? list_path then + logger.error "\"#{list_path}\" does not exist." + return false + end + + list_file_name = "#{list_path}/#{package_name}.list" + list_file = Dir.glob(list_file_name)[0] + directories = [] + + if not list_file.nil? then + File.open(list_file, "r") do |file| + file.each_line do |f| + f = f.strip + if f.nil? or f.empty? then next end + file_path = File.join(target_path, f) + if File.directory? file_path then + if File.symlink? file_path then + File.unlink file_path + next + end + entries = Dir.entries(file_path) + if entries.include? "." then entries.delete(".") end + if entries.include? ".." then entries.delete("..") end + if entries.empty? or entries.nil? then + begin + Dir.rmdir(file_path) + rescue SystemCallError + logger.warn "\"#{file_path}\" directory is not empty" + end + else directories.push(file_path) end + elsif File.file? file_path then FileUtils.rm_f(file_path) + elsif File.symlink? file_path then File.unlink file_path + # if files are already removed by remove script, + else logger.warn "\"#{file_path}\" does not exist" end + end + end + + directories.reverse.each do |path| + if not File.directory? path then next end + entries = Dir.entries(path) + if entries.include? "." then entries.delete(".") end + if entries.include? ".." then entries.delete("..") end + if entries.empty? or entries.nil? then + begin + Dir.rmdir(path) + rescue SystemCallError + logger.warn "\"#{file_path}\" directory is not empty" + end + else next end + end + end + Utils.execute_shell("rm -rf #{list_path}") + return true + end + + def FileInstaller.uninstall(package_name, type, target_path, logger) + case type + when "binary" then + result = execute_remove_script(package_name, target_path, logger) + if result.nil? then return false end + if not remove_pkg_files(package_name, target_path, logger) then return false end + when "source" then + end + + return true + end + + def FileInstaller.move_dir(package_name, source_path, target_path, logger) + config_path = File.join(target_path, PACKAGE_INFO_DIR, package_name) + pkginfo_path = File.join(source_path, PACKAGE_MANIFEST) + data_path = File.join(source_path, "data") + log = "" + + if not File.exist? pkginfo_path then + logger.error "#{PACKAGE_MANIFEST} file does not exist. Check #{source_path}" + return nil + else FileUtils.cp pkginfo_path, config_path end + + if File.exist? data_path then + if Dir.entries(data_path).count > 2 then + # if os is linux, use cpio. it is faster than cp + if Utils.is_linux_like_os( Utils::HOST_OS ) then + absolute_path = `readlink -f #{target_path}` + result = Utils.execute_shell_return("cd #{data_path}; find . -depth | cpio -pldm #{absolute_path}") + else + result = Utils.execute_shell_return("cp -r #{data_path}/* #{target_path}") + end + if result.nil? then + logger.error "Failed to move files" + logger.info " [from: #{source_path}]" + logger.info " [to: #{target_path}]" + return nil + end + logger.info "Moved files.. OK" + log = log + "[from: #{source_path}]\n" + log = log + "[to: #{target_path}]\n" + end + else logger.warn "\"data\" directory does not exist." end + + return log + end + + def FileInstaller.extract_file(package_name, package_file_path, path, target_path, logger) + dirname = File.dirname(package_file_path) + filename = File.basename(package_file_path) + ext = File.extname(filename) + + target_config_path = target_path + "/#{PACKAGE_INFO_DIR}/#{package_name}" + if not File.exist? target_config_path then FileUtils.mkdir_p(target_config_path) end + pkg_file_list = "#{package_name}.list" + pkg_file_list_path = File.join(target_config_path, pkg_file_list) + temp_pkg_file_list = "temp_file_list" + temp_pkg_file_list_path = File.join(target_config_path, "temp_file_list") + + show_file_list_command = nil + extrach_file_list_command = nil + log = "" + + case ext + when ".zip" then + show_file_list_command = "zip -sf #{package_file_path}" + extract_file_list_command = "unzip -o \"#{package_file_path}\" -d \"#{path}\"" + when ".tar" then + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(package_file_path) + _path = Utils.get_unix_path(path) + show_file_list_command = "tar -tf #{_package_file_path}" + extract_file_list_command = "tar xf \"#{_package_file_path}\" -C \"#{_path}\"" + else + logger.error "\"#{filename}\" is not supported." + return nil + end + + system "#{show_file_list_command} > #{temp_pkg_file_list_path}" + File.open(pkg_file_list_path, "a+") do |targetfile| + File.open(temp_pkg_file_list_path, "r") do |sourcefile| + sourcefile.each_line do |l| + if l.strip.start_with? "data/" then + ml = l.strip[5..-1] + targetfile.puts ml + else next end + end + end + end + File.delete(temp_pkg_file_list_path) + + case ext + when ".zip" then + if Utils.is_windows_like_os( Utils::HOST_OS ) then + log = unzip_file(package_file_path, path) + else + #result = Utils.execute_shell_return(extract_file_list_command) + #if result.nil? then log = nil + #else log = result.join("") end + log = `#{extract_file_list_command}` + end + when ".tar" then + #result = Utils.execute_shell_return(extract_file_list_command) + #if result.nil? then log = nil + #else log = result.join("") end + log = `#{extract_file_list_command}` + end + + if log == "" then log = nil end + if log.nil? then + logger.error "Failed to extract \"#{filename}\" file" + logger.info " [file: #{package_file_path}]" + logger.info " [from: #{path}]" + logger.info " [to: #{target_path}]" + logger.info " [cmd: #{extract_file_list_command}]" + return nil + end + + logger.info "Extracted \"#{filename}\" file.. OK" + log = log + "[file: #{package_file_path}]\n" + log = log + "[from: #{path}]\n" + log = log + "[to: #{target_path}]\n" + log = log + "[cmd: #{extract_file_list_command}]\n" + return log + end + + def FileInstaller.extract_a_file(package_file_path, target_file, path, logger) + dirname = File.dirname(package_file_path) + filename = File.basename(package_file_path) + ext = File.extname(filename) + + case ext + when ".zip" then + if not path.nil? then + extract_file_command = "unzip -x #{package_file_path} #{target_file} -d #{path}" + else + extract_file_command = "unzip -x #{package_file_path} #{target_file}" + end + when ".tar" then + # path should be unix path if it is used in tar command + _package_file_path = Utils.get_unix_path(package_file_path) + _path = Utils.get_unix_path(path) + if not path.nil? then + extract_file_command = "tar xf #{_package_file_path} -C #{_path} #{target_file}" + else + extract_file_command = "tar xf #{_package_file_path} #{target_file}" + end + end + + system "#{extract_file_command}" + + if not path.nil? then + target_file_path = File.join(path, target_file) + else + target_file_path = target_file + end + + if File.exist? target_file_path then + logger.info "Extracted \"#{target_file}\" file.." + return true + else + logger.warn "Failed to extracted \"#{target_file}\" file.." + logger.info " [file: #{package_file_path}]" + logger.info " [path: #{path}]" + logger.info " [cmd: #{extract_file_command}]" + return false + end + end + + def FileInstaller.unzip_file(zipfile, dest) + log = "" + Zip::ZipFile.open(zipfile) do |zip_file| + zip_file.each do |f| + f_path = File.join(dest, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + if File.exist?(f_path) then + log = log + "[Warn] Exist file : #{f_path}\n" unless f_path.end_with? "/" + else + zip_file.extract(f, f_path) + if not f_path.end_with? "/" then + log = log + "[info] Extracted file : #{f_path}\n" + end + end + end + end + return log + end + + def FileInstaller.unzip_a_file(zipfile, file, dest) + Zip::ZipFile.open(zipfile) do |zip_file| + zip_file.each do |f| + if f.name.strip == file then + f_path = File.join(dest, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + zip_file.extract(f, f_path) unless File.exist?(f_path) + break + end + end + end + end end - diff --git a/src/pkg_server/packageServer.rb b/src/pkg_server/packageServer.rb index dce1b6b..e490275 100644 --- a/src/pkg_server/packageServer.rb +++ b/src/pkg_server/packageServer.rb @@ -1,6 +1,6 @@ =begin - - packageServer.rb + + packageServer.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -29,329 +29,345 @@ Contributors: require 'fileutils' $LOAD_PATH.unshift File.dirname(__FILE__) $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/build_server" +require "BuildComm" require "packageServerLog" -require "serverConfig" +require "packageServerConfig" require "distribution" +require "SocketRegisterListener" require "client" require "utils" require "mail" +require "DistSync" class PackageServer - attr_accessor :id, :location - + attr_accessor :id, :location, :log, :integrity + attr_accessor :finish, :port + attr_accessor :incoming_path + attr_accessor :distribution_list + attr_accessor :sync_interval, :passwd + # constant - SUPPORTED_OS = ["linux", "windows", "darwin"] + SERVER_ROOT = "#{PackageServerConfig::CONFIG_ROOT}/pkg_server" + DIBS_LOCK_FILE_PATH = "#{SERVER_ROOT}/.server_loc" # initialize def initialize (id) - @id = id @location = "" @distribution_list = [] # distribution name -> server_url hash @dist_to_server_url = {} - - if not File.exist? $server_root then - FileUtils.mkdir_p( $server_root ) + @integrity = "YES" + @auto_sync_flag = "NO" + @finish = false + @port = 3333 + @test_time=0 #test time in mili-seconds + @lock_file= nil + @sync_interval = 3600 + @passwd = "" + + update_config_information(id) + + if not File.exist?( SERVER_ROOT ) + FileUtils.mkdir_p( SERVER_ROOT ) end - @log = PackageServerLog.new( "#{$server_root}/.#{@id}.log", $stdout) + @log = PackageServerLog.new( @log_file_path ) server_information_initialize() - set_distribution_list() end # create - def create (id, dist_name, server_url, loc = nil ) - @id = id + def create( id, dist_name, server_url, loc = nil ) + update_config_information(id) + if loc.nil? or loc.empty? then - @location = Dir.pwd + "/" + id + @location = Dir.pwd + "/" + @id else - if loc.end_with? "/" then - @location = loc + id - else - @location = loc + "/" + id + if Utils.is_absolute_path(loc) then + @location = File.join(loc, @id) + else + @location = File.expand_path(File.join(Dir.pwd, loc, @id)) end end + # error check : check for already exist in server @id + if File.exist? @config_dir + raise RuntimeError, "Server create fail. server id [#{@id}] is already exist" + end + + # name check + if dist_name.strip.eql? "distribution.info" then + raise RuntimeError, "id \"distribution.info\" is not available" + end + # create locking file - File.open("#{$server_create_loc_file}", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - # error check : check for already exist in server id - if File.exist? "#{$server_root}/#{id}" - raise RuntimeError, "Server create fail. server id [#{id}] is already exist" - end - - # error check : check for already exist in server directory - if File.exist? "#{@location}/#{dist_name}" - raise RuntimeError, "Server create fail. directory is already exist [#{@location}/#{dist_name}]" - end - - # create server config directory - FileUtils.mkdir_p "#{$server_root}/#{id}" - FileUtils.mkdir_p "#{$server_root}/#{id}/incoming" - - if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) - # if server_url is local server address then generate absoulte path - if not Utils.is_absolute_path( server_url ) then - if server_url.end_with?("/") then - server_url = Utils::WORKING_DIR + server_url - else - server_url = Utils::WORKING_DIR + "/" + server_url - end - end - end + lock_file = Utils.file_lock(DIBS_LOCK_FILE_PATH) - # create server configure file - File.open( "#{$server_root}/#{id}/config", "w" ) do |f| - f.puts "location : #{@location}" - f.puts "server_url : #{dist_name} -> #{server_url}" - end - - # create location's directory - FileUtils.mkdir_p "#{@location}" - - create_distribution_struct( dist_name, server_url ) - } + # create server config directory + FileUtils.mkdir_p @config_dir + FileUtils.mkdir_p @incoming_path + + if (not server_url.empty?) and \ + (not Utils.is_url_remote(server_url)) and \ + (not Utils.is_absolute_path(server_url)) then + # if server_url is local server address then generate absoulte path + server_url = File.join(Utils::WORKING_DIR, server_url) + end + + # create server configure file + File.open( @config_file_path, "w" ) do |f| + f.puts "location : #{@location}" + f.puts "integrity check : #{@integrity}" + f.puts "auto sync : #{@auto_sync_flag}" + f.puts "sync interval : #{@sync_interval}" + f.puts "server_url : #{dist_name} -> #{server_url}" + end + + # create location's directory + FileUtils.mkdir_p "#{@location}" + + create_distribution_struct( dist_name, server_url ) + Utils.file_unlock(lock_file) + + @log.output( "package server [#{@id}] created successfully", Log::LV_USER ) end - def register( source_pkg_file_path_list, binary_pkg_file_path_list, dist_name, snapshot, test ) + def register( file_path_list, dist_name, snapshot, test_flag, internal_flag = false ) @log.info "package register in server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) - # distribution lock - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - source_pkg_file_name_list = [] - updated_os_list = [] - append_pkg_list = [] - used_source_pkg_list = [] - package_list = [] - - # error check - source_pkg_file_path_list.each do |l| - # error check for file exist - if not File.exist? l - raise RuntimeError, "source package file does not exist [#{l}]" - end - - source_pkg_file_name_list.push File.basename( l ) - end - binary_pkg_file_path_list.each do |l| - # error check for file exist - if not File.exist? l - raise RuntimeError, "binary package file does not exist [#{l}]" - end - end - - # register binary package - binary_pkg_file_path_list.each do |l| - # get package class using bianry file - pkg = distribution.get_package_from_file(l) - - if pkg.nil? or pkg.package_name.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - end - package_list.push pkg - - if test then - if not pkg.source.empty? then - if not source_pkg_file_name_list.include? "#{pkg.source}_#{pkg.version}.tar.gz" - raise "binary package and source package must be upload same time" - end - - pkg.src_path = "/temp/#{pkg.source}_#{pkg.version}.tar.gz" - end - pkg = distribution.register_for_test(l ,pkg ) + # distribution lock + @lock_file = Utils.file_lock(distribution.lock_file_path) + + updated_os_list = [] + registed_package_list = [] + binary_pkg_file_path_list = [] + link_pkg_file_path_list = [] + archive_pkg_file_path_list = [] + snapshot_name = "" + + file_path_list.each do |f| + # error check for file exist + if not File.exist? f + raise RuntimeError, "package file does not exist [#{f}]" + end + + pkg = distribution.get_package_from_file(f) + + # binary package + if not pkg.nil? then + + # find link package + pkg_os = Utils.get_os_from_package_file(f) + link_pkg = distribution.get_link_package(pkg, pkg_os) + if link_pkg.nil? then + binary_pkg_file_path_list.push f else - if pkg.package_name.empty? or pkg.version.empty? or pkg.os.empty? or pkg.maintainer.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - # binary only package - elsif pkg.attribute.include? "binary" then - @log.info "binary package [#{l}] is binary only package" - pkg.src_path = "" - elsif pkg.source.empty? then - raise "[#{l}]'s pkginfo.manifest file is incomplete." - # binary package - else - if not source_pkg_file_name_list.include? "#{pkg.source}_#{pkg.version}.tar.gz" - raise "binary package [#{pkg.package_name}]'s source package must be upload same time" - end - - @log.info "binary package [#{l}]'s source package is #{pkg.source}" - used_source_pkg_list.push "#{pkg.source}_#{pkg.version}.tar.gz" - pkg.src_path = "/source/#{pkg.source}_#{pkg.version}.tar.gz" - end - - pkg = distribution.register(l ,pkg ) - updated_os_list.push pkg.os + link_pkg_file_path_list.push [link_pkg.path, File.basename(f)] + pkg.checksum = link_pkg.checksum + pkg.size = link_pkg.size end - - append_pkg_list.push pkg - end - - # check install dependency integrity - if not test then distribution.check_instll_dependency_integrity end - - source_pkg_file_path_list.each do |source_path| - source_name = File.basename(source_path) - if File.exist? "#{@location}/#{dist_name}/source/#{source_name}" then - @log.warn "source package already exist then does not register" - next - end - - if test then - @log.info "source package [#{source_name}] register in temp/]" - FileUtils.cp( source_path, "#{@location}/#{dist_name}/temp/" ) + + # update os information + if pkg.os_list.include? pkg_os then + pkg.os = pkg_os + pkg.os_list = [pkg_os] else - @log.info "source package [#{source_name}] register in source/]" - FileUtils.cp( source_path, "#{@location}/#{dist_name}/source/" ) + raise RuntimeError, "package file name is incorrect [#{f}]" end - end - # register archive pakcage list. - distribution.write_archive_pkg_list( source_pkg_file_name_list - used_source_pkg_list ) + updated_pkg = register_package(distribution, pkg, f, test_flag, internal_flag) - binary_pkg_file_path_list.each do |l| - if test then - FileUtils.cp( l, "#{@location}/#{dist_name}/temp/" ) - else - FileUtils.cp( l, "#{@location}/#{dist_name}/binary/" ) + updated_os_list.push updated_pkg.os + registed_package_list.push updated_pkg + # archive package + else + if test_flag then + @log.error("archive package does not using test mode", Log::LV_USER) + return end + + file_name = File.basename(f) + distribution.register_archive_pkg(file_name) + archive_pkg_file_path_list.push f end + end + + # check install dependency integrity + if not test_flag and @integrity.eql? "YES" then + registed_package_list.each do |pkg| + distribution.check_package_integrity(pkg) + end + end + + # move file to package server + binary_pkg_file_path_list.each do |l| + if test_flag then + FileUtils.copy_file( l, "#{distribution.location}/temp/#{File.basename(l)}" ) + else + FileUtils.copy_file( l, "#{distribution.location}/binary/#{File.basename(l)}" ) + end + end + + # link to package server + link_pkg_file_path_list.each do |l| + if test_flag then + src_file = File.join(distribution.location, l[0]) + dest_file = File.join(distribution.location, "temp", l[1]) + FileUtils.ln( src_file, dest_file, :force => true ) + else + src_file = File.join(distribution.location, l[0]) + dest_file = File.join(distribution.location, "binary", l[1]) + FileUtils.ln( src_file, dest_file, :force => true ) + end + end + + archive_pkg_file_path_list.each do |l| + FileUtils.mv( l, "#{distribution.location}/source/" ) + end - # write package list for updated os - updated_os_list.uniq! - updated_os_list.each do |os| - distribution.write_pkg_list( os ) + # write package list for updated os + updated_os_list.uniq! + updated_os_list.each do |os| + distribution.write_pkg_list(os) + end + + # register archive pakcage list. + distribution.write_archive_pkg_list() + + # send email + if test_flag then + msg_list = [] + + registed_package_list.each do |p| + msg_list.push("%-30s: %08s" % [ p.package_name.strip, p.version.strip ] ) end + # email just remote package server + # Mail.send_package_registe_mail( msg_list, @id ) + end + + # if snapshot mode is true then generate snapshot + if snapshot or test_flag then + @log.info "generaging snapshot" + snapshot_name = distribution.generate_snapshot("", "", false, "Changed package: \n" + registed_package_list.map{|x|"- #{x.package_name}"}.join("\n") + "\n\n" + get_changelog_string(registed_package_list) ) + end - # if snapshot mode is true then generate snapshot - if snapshot or test then - @log.info "generaging snapshot" - distribution.generate_snapshot("", "", "") - end - - # send email - if not test then - msg_list = [] - - package_list.map{ |p| - msg_list.push("%-30s: %08s" % [ p.package_name.strip, p.version.strip ] ) - } - # email just remote package server - # Mail.send_package_registe_mail( msg_list, @id ) + Utils.file_unlock(@lock_file) + @log.output( "package registed successfully", Log::LV_USER) + + return snapshot_name + end + + def get_changelog_string( package_list ) + log_list = {} + package_list.each do |pkg| + if not pkg.does_change_exist? then next end + set = false + if log_list[[pkg.package_name, pkg.version, pkg.get_changes]].nil? then + log_list[[pkg.package_name, pkg.version, pkg.get_changes]] = pkg.os_list + else + log_list[[pkg.package_name, pkg.version, pkg.get_changes]] = log_list[[pkg.package_name, pkg.version, pkg.get_changes]] + pkg.os_list end - } - end + end + str="" + log_list.each do |key, os_list| + str = str + "Package: #{key[0]}\nOS: #{os_list.join(", ")}\nVersion: #{key[1]}\nChanges: \n#{key[2].sub(/^==/,'Uploader:')}\n\n" + end + return str + end - def generate_snapshot( snpashot_name, dist_name, base_snapshot, binary_pkg_file_path_list) + def generate_snapshot( snpashot_name, dist_name, base_snapshot ) @log.info "generating snapshot" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - append_pkg_list = [] - binary_pkg_file_path_list.each do |l| - if not File.exist? l then raise RuntimeError,"Can't find binary package file [#{l}]" end - pkg = distribution.get_package_from_file(l) - if l.start_with? "/" - pkg.path = "#{l}" - else - pkg.path = "#{Dir.pwd}/#{l}" - end - append_pkg_list.push pkg - end - - distribution.generate_snapshot( snpashot_name, base_snapshot, append_pkg_list) - } + @lock_file = Utils.file_lock(distribution.lock_file_path) + + snapshot_name = distribution.generate_snapshot( snpashot_name, base_snapshot, true, "SYSTEM:") + + Utils.file_unlock(@lock_file) + + return snapshot_name end - def sync( dist_name, mode ) + def sync( dist_name, mode, snapshot = "" ) @log.info "sync from server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end - distribution = get_distribution( dist_name ) - - if distribution.server_url.empty? then - @log.error( "This distribution has not remote server", Log::LV_USER) + distribution = get_distribution( dist_name ) + + if distribution.server_url.empty? then + @log.error( "This distribution has not remote server", Log::LV_USER) return - end - - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - distribution.sync( mode, "linux" ) - distribution.sync( mode, "windows" ) - distribution.sync( mode, "darwin" ) - distribution.sync_archive_pkg - } + end + + begin + ret = distribution.sync(mode, snapshot) + + if ret then + distribution.generate_snapshot("", "", false, distribution.last_sync_changes) + end + + @log.output( "package server [#{@id}]'s distribution [#{dist_name}] has been synchronized.", Log::LV_USER ) + rescue => e + @log.error( e.message, Log::LV_USER) + end end def add_distribution( dist_name, server_url, clone ) - File.open("#{$server_create_loc_file}", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) - - # error check : check for already exist in server directory - if @dist_to_server_url.keys.include? dist_name.strip then - raise RuntimeError, "distribution already exist : #{dist_name}" - end - if File.exist? "#{@location}/#{dist_name}" - raise RuntimeError, "distribution directory already exist [#{@location}/#{dist_name}]" - end - - if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) - # if server_url is local server address then generate absoulte path - if not Utils.is_absolute_path( server_url ) then - if server_url.end_with?("/") then - server_url = Utils::WORKING_DIR + server_url - else - server_url = Utils::WORKING_DIR + "/" + server_url - end - end - end + lock_file = Utils.file_lock(@server_lock_file_path) - File.open( "#{$server_root}/#{@id}/config", "a" ) do |f| - if clone then - @log.info "add distribution using [#{server_url}] in clone mode" - f.puts "server_url : #{dist_name} -> " + # error check : check for already exist in server directory + if @dist_to_server_url.keys.include? dist_name.strip then + Utils.file_unlock(@lock_file) + raise RuntimeError, "distribution already exist : #{dist_name}" + end + + # name check + if dist_name.strip.eql? "distribution.info" then + Utils.file_unlock(@lock_file) + raise RuntimeError, "id \"distribution.info\" is not available" + end + + # modify server url + if (not server_url.empty?) and (not Utils.is_url_remote(server_url)) + # if server_url is local server address then generate absoulte path + if not Utils.is_absolute_path( server_url ) then + if server_url.end_with?("/") then + server_url = Utils::WORKING_DIR + server_url else - @log.info "add distribution using [#{server_url}]" - f.puts "server_url : #{dist_name} -> #{server_url}" + server_url = Utils::WORKING_DIR + "/" + server_url end end - - create_distribution_struct( dist_name, server_url ) - } + end + + add_dist_for_config_file(dist_name, server_url, clone) + create_distribution_struct( dist_name, server_url ) + + Utils.file_unlock(lock_file) + @log.output( "distribution [#{dist_name}] added successfully", Log::LV_USER ) end - def remove_server( id ) - @log.info( "Package server [#{id}] will be removed and all server information delete", Log::LV_USER) + def add_os(dist_name, os) + dist = get_distribution(dist_name) - if File.exist? "#{$server_root}/#{id}/config" then - File.open "#{$server_root}/#{id}/config" do |f| + # distribution lock + @lock_file = Utils.file_lock(dist.lock_file_path) + + dist.add_os(os) + + @log.info "generaging snapshot" + dist.generate_snapshot("", "", false, "Add #{os} Package server") + + Utils.file_unlock(@lock_file) + @log.output( "package server add os [#{os}] successfully", Log::LV_USER ) + end + + def remove_server() + @log.info( "Package server [#{@id}] will be removed and all server information delete", Log::LV_USER) + + lock_file = Utils.file_lock(DIBS_LOCK_FILE_PATH) + if File.exist? @config_file_path then + File.open @config_file_path do |f| f.each_line do |l| - if l.start_with?( "location : ") then + if l.start_with?( "location : ") then location= l.split(" : ")[1] FileUtils.rm_rf l.split(" : ")[1].strip @log.info( "server location removed : #{location}", Log::LV_USER) @@ -359,91 +375,245 @@ class PackageServer end end else - @log.error( "Can't find server information : #{id}", Log::LV_USER) - end - - FileUtils.rm_rf "#{$server_root}/.#{id}.log" - FileUtils.rm_rf "#{$server_root}/#{id}" + @log.error( "Can't find server information : #{@id}", Log::LV_USER) + end + + FileUtils.rm_rf @config_dir + FileUtils.rm_rf @log_file_path + + Utils.file_unlock(lock_file) + @log.output( "package server [#{@id}] removed successfully", Log::LV_USER ) end - def remove_pkg( id, dist_name, pkg_name_list ) + def remove_dist( dist_name ) + @log.info "remove distribution in server" + distribution = get_distribution( dist_name ) + + lock_file = Utils.file_lock(@server_lock_file_path) + + # modify config file + config_file = File.readlines(@config_file_path) + File.open(@config_file_path, 'w') do |f| + config_file.each do |line| + f.puts(line) if not line =~ /server_url : #{dist_name} ->/ + end + end + + # modify info file + config_file = File.readlines("#{@location}/distribution.info") + File.open("#{@location}/distribution.info", 'w') do |f| + remove_flag = false + config_file.each do |line| + if line.start_with? "name :" then + if line.split(':')[1].strip.eql? dist_name then + remove_flag = true + else + remove_flag = false + end + + end + + # rewrite information for not remove distribution + if not remove_flag then + f.puts line + end + end + end + + # remove distribution directory + FileUtils.rm_rf distribution.location + + # remove distribution struct + @distribution_list.delete distribution + + Utils.file_unlock(lock_file) + end + + def remove_pkg( dist_name, pkg_name_list, os ) @log.info "package remove in server" - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end distribution = get_distribution( dist_name ) - # distribution lock - File.open("#{@location}/#{dist_name}/.lock_file", File::RDWR|File::CREAT, 0644) {|f| - f.flock(File::LOCK_EX) - f.rewind - f.flush - f.truncate(f.pos) + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.remove_pkg(pkg_name_list, os) - distribution.remove_pkg(pkg_name_list) - } + # generate snapshot + @log.info "generaging snapshot" + distribution.generate_snapshot("", "", false, "SYSTEM: Package \"#{pkg_name_list.join(", ")}\" is(are) removed in #{os} server") + + Utils.file_unlock(lock_file) + @log.output( "package removed successfully", Log::LV_USER ) end - def find_source_package_path( dist_name, pkg_file_name_list ) - if dist_name.empty? then dist_name = get_default_dist_name() end - if dist_name.empty? then raise RuntimeError,"Can't find distribution information" end + def remove_snapshot( dist_name, snapshot_list ) + @log.info "remove snapshot in server" distribution = get_distribution( dist_name ) - pkg_file_name_list.each do |pkg| - pkg_path = "#{@location}/#{dist_name}/source/#{pkg}" - if File.exist? pkg_path then - @log.info( "#{pkg}", Log::LV_USER) + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.remove_snapshot(snapshot_list) + + Utils.file_unlock(lock_file) + end + + def clean( dist_name, snapshot_list ) + @log.info "pakcage server clean" + distribution = get_distribution( dist_name ) + + lock_file = Utils.file_lock(@server_lock_file_path) + + distribution.clean( snapshot_list ) + + # remove incoming dir + FileUtils.rm_rf incoming_path + FileUtils.mkdir incoming_path + + Utils.file_unlock(lock_file) + end + + # start server daemon + def start( port, passwd ) + @log.info "Package server Start..." + # set port number. default port is 3333 + @port = port + + # set job request listener + @log.info "Setting listener..." + listener = SocketRegisterListener.new(self) + listener.start + + # set auto sync + if @auto_sync_flag.eql? "YES" then + @log.info "Setting auto sync..." + autosync = DistSync.new(self) + autosync.start + end + + # set password + @passwd = passwd + + # main loop + @log.info "Entering main loop..." + if @test_time > 0 then start_time = Time.now end + while( not @finish ) + # sleep + if @test_time > 0 then + curr_time = Time.now + if (curr_time - start_time).to_i > @test_time then + puts "Test time is elapsed!" + break + end else - @log.error( "Can't find [#{pkg}] in source package", Log::LV_USER) + sleep 1 end - end + end end - def PackageServer.list_id - @@log = PackageServerLog.new( "#{$server_root}/.log", $stdout) + # stop server daemon + def stop( port, passwd ) + # set port number. default port is 3333 + @port = port + @finish = false - d = Dir.new( $server_root ) - s = d.select {|f| not f.start_with?(".") } + client = BuildCommClient.create("127.0.0.1", @port, @log) + if client.nil? then + raise RuntimeError, "Server does not listen in #{@port} port" + end + + client.send("STOP|#{passwd}") + + ret = client.receive_data + if not ret.nil? and ret[0].strip.eql? "SUCC" then + @log.output( "Package server is stopped", Log::LV_USER) + else + @log.output( "Package server return error message! #{ret}", Log::LV_USER) + if not client.get_error_msg().empty? then + @log.output( "Error: #{client.get_error_msg()}", Log::LV_USER) + end + end + client.terminate + + end + + def self.list_id + @@log = PackageServerLog.new("#{SERVER_ROOT}/.log") + + d = Dir.new( SERVER_ROOT ) + s = d.select {|f| not f.start_with?(".") } s.sort! - + server_list = [] + @@log.output( "=== server ID list ===", Log::LV_USER) s.each do |id| + if File.basename(id).include?(".log") then next end + + server_list.push id @@log.output( id, Log::LV_USER) - end + end + @@log.close + FileUtils.rm_rf("#{SERVER_ROOT}/.log") + + return server_list end - def PackageServer.list_dist( id ) - @@log = PackageServerLog.new( "#{$server_root}/.log", $stdout) - + def self.list_dist( id ) + @@log = PackageServerLog.new( "#{SERVER_ROOT}/.log" ) + @@log.output( "=== ID [#{id}]'s distribution list ===", Log::LV_USER) + dist_list = [] + # read package id information - if File.exist? "#{$server_root}/#{id}/config" then - File.open "#{$server_root}/#{id}/config" do |f| - f.each_line do |l| - if l.start_with?( "server_url : ") and l.include?( "->" ) then - @@log.output( l.split(" : ")[1].split("->")[0], Log::LV_USER) - end + config_file_path = "#{SERVER_ROOT}/#{id}/config" + if not File.exist? config_file_path + raise RuntimeError, "[#{id}] is not server ID" + end + + File.open config_file_path do |f| + f.each_line do |l| + if l.start_with?( "server_url : ") and l.include?( "->" ) then + dist_name = l.split(" : ")[1].split("->")[0] + + dist_list.push dist_name + @@log.output( dist_name, Log::LV_USER) end end - else - raise RuntimeError, "[#{id}] is not server ID" - end + end + @@log.close + FileUtils.rm_rf("#{SERVER_ROOT}/.log") + + return dist_list end - def get_default_dist_name() - if @distribution_list.empty? then + def get_default_dist_name() + if @distribution_list.empty? then raise RuntimeError,"Server [#{@id}] does not have distribution" - end - return @distribution_list[0].name + end + return @distribution_list[0].name + end + + def reload_dist_package() + # create locking file + lock_file = Utils.file_lock(@server_lock_file_path) + @distribution_list.each do |dist| + dist.initialize_pkg_list + end + Utils.file_unlock(lock_file) + end + + def release_lock_file + if not @lock_file.nil? then + Utils.file_unlock(@lock_file) + end end # PRIVATE METHODS/VARIABLES - private + private - def server_information_initialize + def server_information_initialize # if id is nil or empty then find default id - if @id.nil? or @id.empty? - d = Dir.new( $server_root ) + if @id.nil? or @id.empty? + d = Dir.new( SERVER_ROOT ) s = d.select {|f| not f.start_with?(".") } if s.length.eql? 1 then @log.info "using default server ID [#{s[0]}]" @@ -454,32 +624,43 @@ class PackageServer end # read package id information - if File.exist? $server_root and File.exist? "#{$server_root}/#{@id}/config" then - File.open "#{$server_root}/#{@id}/config" do |f| + if File.exist? @config_file_path + File.open @config_file_path do |f| f.each_line do |l| - if l.start_with?( "location : ") then - @location = l.split(" : ")[1].strip - elsif l.start_with?( "server_url : " ) then - info = l.split(" : ")[1].split("->") - @dist_to_server_url[info[0].strip] = info[1].strip + if l.start_with?( "location :") then + @location = l.split(" :")[1].strip + elsif l.start_with?( "integrity check :") then + @integrity = l.split(" :")[1].strip.upcase + elsif l.start_with?( "auto sync :" ) then + @auto_sync_flag = l.split(" :")[1].strip.upcase + elsif l.start_with?( "sync interval :" ) then + @sync_interval = l.split(" :")[1].strip.to_i + elsif l.start_with?( "server_url :" ) then + info = l.split(" :")[1].split("->") + @dist_to_server_url[info[0].strip] = info[1].strip else @log.error "server config file has invalid information [#{l}]" end end end - end - end - def set_distribution_list - @dist_to_server_url.each do |dist_name, server_url| - @distribution_list.push Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, @log) + @dist_to_server_url.each do |dist_name, server_url| + @distribution_list.push Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, self ) + end end end def get_distribution( dist_name ) + if dist_name.nil? or dist_name.empty? then + dist_name = get_default_dist_name() + end + if dist_name.empty? then + raise RuntimeError,"Can't find distribution information" + end + @distribution_list.each do |dist| if dist.name.eql? dist_name.strip - return dist + return dist end end @@ -487,40 +668,91 @@ class PackageServer end def create_distribution_struct( dist_name, server_url ) + if File.exist? "#{@location}/#{dist_name}" + raise RuntimeError, "distribution directory already exist [#{@location}/#{dist_name}]" + end + FileUtils.mkdir "#{@location}/#{dist_name}" FileUtils.mkdir "#{@location}/#{dist_name}/binary" FileUtils.mkdir "#{@location}/#{dist_name}/source" FileUtils.mkdir "#{@location}/#{dist_name}/temp" FileUtils.mkdir "#{@location}/#{dist_name}/snapshots" - + File.open("#{@location}/#{dist_name}/#{Distribution::SNAPSHOT_INFO_FILE}", "w") {} + File.open("#{@location}/#{dist_name}/#{Distribution::OS_INFO_FILE}", "w") {} + File.open("#{@location}/#{dist_name}/#{Distribution::ARCHIVE_PKG_FILE}", "w") {} + # generate distribution - distribution = Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, @log ) + distribution = Distribution.new( dist_name, "#{@location}/#{dist_name}", server_url, self ) # add dist @distribution_list.push distribution - + if not server_url.empty? then @log.info "generate package server using remote package server [#{server_url}]" - if Utils.is_url_remote(server_url) then + if Utils.is_url_remote(server_url) then @log.info "[#{dist_name}] distribution creation. using remote server [#{server_url}]" - else + else @log.info "[#{dist_name}] distribution creation. using local server [#{server_url}]" end - distribution.sync( false, "linux" ) - distribution.sync( false, "windows" ) - distribution.sync( false, "darwin" ) - distribution.sync_archive_pkg + distribution.sync(false) + distribution.generate_snapshot("", "", false, distribution.last_sync_changes) else @log.info "generate package server do not using remote package server" - + # write_pkg_list for empty file - distribution.write_pkg_list( "linux" ) - distribution.write_pkg_list( "windows" ) - distribution.write_pkg_list( "darwin" ) - distribution.write_archive_pkg_list( "" ) + distribution.write_pkg_list(nil) + distribution.write_archive_pkg_list() + end + + # add dist information to distribution.info file + File.open("#{@location}/distribution.info", "a") do |f| + f.puts "name : #{dist_name}" + f.puts "time : #{Time.now.strftime("%Y%m%d%H%M%S")}" + f.puts end end -end + def register_package(distribution, pkg, file_path, test_flag, internal_flag) + # get package class using bianry file + if pkg.nil? or pkg.package_name.empty? then + raise "[#{file_path}]'s pkginfo.manifest file is incomplete." + end + + if not test_flag then + # error check + if pkg.package_name.empty? or pkg.version.empty? \ + or pkg.os.empty? or pkg.maintainer.empty? then + raise "[#{file_path}]'s pkginfo.manifest file is incomplete." + end + + updated_pkg = distribution.register(file_path, pkg, internal_flag ) + else + updated_pkg = distribution.register_for_test(file_path, pkg ) + end + + return updated_pkg + end + + def add_dist_for_config_file(dist_name, server_url, clone) + File.open( @config_file_path, "a" ) do |f| + if clone then + @log.info "add distribution using [#{server_url}] in clone mode" + f.puts "server_url : #{dist_name} -> " + else + @log.info "add distribution using [#{server_url}]" + f.puts "server_url : #{dist_name} -> #{server_url}" + end + end + end + + def update_config_information(id) + @id = id + @config_dir = "#{SERVER_ROOT}/#{@id}" + @log_file_path = "#{SERVER_ROOT}/#{@id}.log" + @config_file_path = "#{@config_dir}/config" + @incoming_path = "#{@config_dir}/incoming" + @server_lock_file_path = "#{@config_dir}/.server_lock" + end +end diff --git a/src/pkg_server/packageServerConfig.rb b/src/pkg_server/packageServerConfig.rb new file mode 100644 index 0000000..3c782a6 --- /dev/null +++ b/src/pkg_server/packageServerConfig.rb @@ -0,0 +1,34 @@ +=begin + + serverConfig.rb + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +$LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" +require "utils" + +class PackageServerConfig + CONFIG_ROOT = "#{Utils::HOME}/.build_tools" +end diff --git a/src/pkg_server/packageServerLog.rb b/src/pkg_server/packageServerLog.rb index b20c798..1ca1068 100644 --- a/src/pkg_server/packageServerLog.rb +++ b/src/pkg_server/packageServerLog.rb @@ -1,6 +1,6 @@ =begin - - packageServerLog.rb + + packageServerLog.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -32,15 +32,15 @@ require "logger" class PackageServerLog < Log - def initialize(path, stream_out) + def initialize(path) super(path) - @extra_out = stream_out + @second_out = $stdout end protected def output_extra(msg) - @extra_out.puts msg - end + @second_out.puts msg + end end diff --git a/src/pkg_server/serverOptParser.rb b/src/pkg_server/serverOptParser.rb index bdacd82..f37d68b 100644 --- a/src/pkg_server/serverOptParser.rb +++ b/src/pkg_server/serverOptParser.rb @@ -1,6 +1,6 @@ =begin - - serverOptParser.rb + + serverOptParser.rb Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. @@ -30,171 +30,224 @@ require 'optparse' $LOAD_PATH.unshift File.dirname(File.dirname(__FILE__))+"/common" require "utils" -def set_default( options ) - if options[:id].nil? then options[:id] = "" end - if options[:dist].nil? then options[:dist] = "" end - if options[:url].nil? then options[:url] = "" end - if options[:os].nil? then options[:os] = "all" end - if options[:bpkgs].nil? then options[:bpkgs] = [] end - if options[:apkgs].nil? then options[:apkgs] = [] end - if options[:spkgs].nil? then options[:spkgs] = [] end - if options[:snap].nil? then options[:snap] = "" end - if options[:bsnap].nil? then options[:bsnap] = "" end - if options[:gensnap].nil? then options[:gensnap] = false end - if options[:force].nil? then options[:force] = false end - if options[:test].nil? then options[:test] = false end - if options[:clone].nil? then options[:clone] = false end +def set_default( options ) + options[:id] = "" + options[:dist] = "" + options[:url] = "" + options[:os] = "all" + options[:pkgs] = [] + options[:snaps] = [] + options[:bsnap] = "" + options[:port] = "3333" + options[:gensnap] = true + options[:force] = false + options[:test] = false + options[:clone] = false + options[:origin_pkg_name] = "" + options[:origin_pkg_os] = "" + options[:passwd] = "" end def option_error_check( options ) - - case options[:cmd] - when "create" - if options[:id].empty? or options[:dist].empty? then - raise ArgumentError, "Usage: pkg-svr create -i -d [-u ] [-l ] " - end - when "remove-pkg" - if options[:bpkgs].empty? then - raise ArgumentError, "pkg-svr remove-pkg -i -d -p " + "\n" \ - end - when "spkg-path" - if options[:spkgs].empty? then - raise ArgumentError, "Usage: pkg-svr spkg-name -i -d -s " + + case options[:cmd] + when "create" + if options[:id].empty? or options[:dist].empty? then + raise ArgumentError, "Usage: pkg-svr create -n -d [-u ] [-l ] " end when "remove" if options[:id].empty? then - raise ArgumentError, "Usage: pkg-svr remove -i " + raise ArgumentError, "Usage: pkg-svr remove -n " + end + when "remove-pkg" + if options[:pkgs].empty? then + raise ArgumentError, "Usage: pkg-svr remove-pkg -n -d -P [-o ]" + "\n" \ + end + when "remove-snapshot" + if options[:snaps].empty? then + raise ArgumentError, "Usage: pkg-svr remove-snapshot -n -d -s " end when "add-dist" - if options[:id].empty? or options[:dist].empty? then - raise ArgumentError, "Usage: pkg-svr add-dist -i -d [-u ] [-c] " + if options[:id].empty? or options[:dist].empty? then + raise ArgumentError, "Usage: pkg-svr add-dist -n -d [-u ] [--clone] " + end + when "add-os" + if options[:os].empty? then + raise ArgumentError, "Usage: pkg-svr add-os -n -d -o ] " + end + when "remove-dist" + if options[:id].empty? or options[:dist].empty? then + raise ArgumentError, "Usage: pkg-svr remove-dist -n -d " end when "register" - if options[:bpkgs].empty? and options[:spkgs].empty? then - raise ArgumentError, "Usage: pkg-svr register -i -d -p -s [-g] [-t] " + if options[:pkgs].empty? then + raise ArgumentError, "Usage: pkg-svr register -n -d -P [--gen] [--test] " end - when "remove" when "gen-snapshot" + if options[:snaps].empty? then + raise ArgumentError, "Usage: pkg-svr gen-snapshot -n -d -s [-b ]" + end + when "start" + if options[:port].empty? then + raise ArgumentError, "Usage: pkg-svr start -n -p [-w ]" + end + when "stop" + if options[:port].empty? then + raise ArgumentError, "Usage: pkg-svr stop -n -p [-w ]" + end when "sync" when "list" + when "clean" else - raise ArgumentError, "input option incorrect : #{options[:cmd]}" + raise ArgumentError, "Input is incorrect : #{options[:cmd]}" end end -def option_parse - options = {} - banner = "Usage: pkg-svr {create|register|gen-snapshot|sync|add-dist|spkg-path|remove|remove-pkg|list|help} ..." + "\n" \ - + "\t" + "pkg-svr create -i -d [-u ] [-l ] " + "\n" \ - + "\t" + "pkg-svr add-dist -i -d [-u ] [-c] " + "\n" \ - + "\t" + "pkg-svr remove -i " + "\n" \ - + "\t" + "pkg-svr register -i -d -p -s [-g] [-t] " + "\n" \ - + "\t" + "pkg-svr remove-pkg -i -d -p " + "\n" \ - + "\t" + "pkg-svr gen-snapshot -i -d [-n ] [-b ] [-p ] " + "\n" \ - + "\t" + "pkg-svr sync -i -d [-f] " + "\n" \ - + "\t" + "pkg-svr spkg-path -i -d -s " + "\n" \ - + "\t" + "pkg-svr list [-i ] " + "\n" - - optparse = OptionParser.new do|opts| - # Set a banner, displayed at the top - # of the help screen. - - opts.banner = banner - - opts.on( '-i', '--id ', 'package server id' ) do|name| - options[:id] = name - end - +def option_parse + options = {} + banner = "Package-server administer service command-line tool." + "\n" \ + + "\n" + "Usage: pkg-svr [OPTS] or pkg-svr (-h|-v)" + "\n" \ + + "\n" + "Subcommands:" + "\n" \ + + "\t" + "create Create a package-server." + "\n" \ + + "\t" + "add-dist Add a distribution to package-server." + "\n" \ + + "\t" + "add-os Add supported os." + "\n" \ + + "\t" + "register Register a package in package-server." + "\n" \ + + "\t" + "remove Remove a package-server." + "\n" \ + + "\t" + "remove-dist Remove a distribution to package-server." + "\n" \ + + "\t" + "remove-pkg Remove a package in package-server." + "\n" \ + + "\t" + "remove-snapshot Remove a snapshot in package-server." + "\n" \ + + "\t" + "gen-snapshot Generate a snapshot in package-server." + "\n" \ + + "\t" + "sync Synchronize the package-server from parent package server." + "\n" \ + + "\t" + "start Start the package-server." + "\n" \ + + "\t" + "stop Stop the package-server." + "\n" \ + + "\t" + "clean Delete unneeded package files in package-server." + "\n" \ + + "\t" + "list Show all pack" + "\n" \ + + "\n" + "Subcommand usage:" + "\n" \ + + "\t" + "pkg-svr create -n -d [-u ] [-l ] " + "\n" \ + + "\t" + "pkg-svr add-dist -n -d [-u ] [--clone] " + "\n" \ + + "\t" + "pkg-svr add-os -n -d -o " + "\n" \ + + "\t" + "pkg-svr register -n -d -P [--gen] [--test] " + "\n" \ + + "\t" + "pkg-svr remove -n " + "\n" \ + + "\t" + "pkg-svr remove-dist -n -d " + "\n" \ + + "\t" + "pkg-svr remove-pkg -n -d -P [-o ] " + "\n" \ + + "\t" + "pkg-svr remove-snapshot -n -d -s " + "\n" \ + + "\t" + "pkg-svr gen-snapshot -n -d -s [-b ] " + "\n" \ + + "\t" + "pkg-svr sync -n -d -s [--force] " + "\n" \ + + "\t" + "pkg-svr clean -n -d [-s ] " + "\n" \ + + "\t" + "pkg-svr start -n -p [-w ]" + "\n" \ + + "\t" + "pkg-svr stop -n -p [-w ]" + "\n" \ + + "\t" + "pkg-svr list [-n ] " + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + # Set a banner, displayed at the top + # of the help screen. + + opts.banner = banner + + opts.on( '-n', '--name ', 'package server name' ) do|name| + options[:id] = name + end + opts.on( '-d', '--dist ', 'package server distribution' ) do|dist| - options[:dist] = dist - end - - opts.on( '-u', '--url ', 'remote server address' ) do|url| - options[:url] = url - end - + options[:dist] = dist + end + + opts.on( '-u', '--url ', 'remote server url: http://127.0.0.1/dibs/unstable' ) do|url| + options[:url] = url + end + opts.on( '-o', '--os ', 'target operating system' ) do|os| - options[:os] = os - end - - opts.on( '-p', '--bpackage ', 'binary package file path list' ) do|bpkgs| - options[:bpkgs] = [] - list = bpkgs.tr(" \t","").split(",") - list.each do |l| - # TODO: is not working - #reg = Regexp.new(l) - #Dir.entries(Utils::WORKING_DIR).select{|x| x =~ reg}.each do |ls| - # options[:bpkgs].push ls - #end - if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:bpkgs].push l - end - end - - opts.on( '-s', '--spackage ', 'source package file path ' ) do|spkgs| - options[:spkgs] = [] - list = spkgs.tr(" \t","").split(",") - list.each do |l| + options[:os] = os + end + + opts.on( '-P', '--pkgs ', 'package file path list' ) do|pkgs| + if not Utils.multi_argument_test( pkgs, "," ) then + raise ArgumentError, "Package variable parsing error : #{pkgs}" + end + list = pkgs.tr(" \t","").split(",") + list.each do |l| if l.start_with? "~" then l = Utils::HOME + l.delete("~") end - options[:spkgs].push l - end - end - - opts.on( '-g', '--generate', 'snapshot is generate' ) do - options[:gensnap] = true - end - - opts.on( '-n', '--sname ', 'snapshot name' ) do|snap| - options[:snap] = snap - end - - opts.on( '-b', '--bsnapshot ', 'base snapshot name' ) do|bsnap| - options[:bsnap] = bsnap - end - - opts.on( '-l', '--location ', 'server location' ) do|loc| - options[:loc] = loc - end - - opts.on( '-f', '--force', 'force update pkg file' ) do - options[:force] = true - end - - opts.on( '-t', '--test', 'upload for test' ) do - options[:test] = true - end - - opts.on( '-c', '--clone', 'clone mode' ) do - options[:clone] = true - end - - opts.on( '-h', '--help', 'display this information' ) do - puts opts + options[:pkgs].push l + end + end + + opts.on( '-s', '--snapshot ', 'a snapshot name or snapshot list' ) do|snaplist| + if not Utils.multi_argument_test( snaplist, "," ) then + raise ArgumentError, "Snapshot variable parsing error : #{snaplist}" + end + options[:snaps] = snaplist.split(",") + end + + opts.on( '-b', '--base ', 'base snapshot name' ) do|bsnap| + options[:bsnap] = bsnap + end + + opts.on( '-l', '--loc ', 'server location' ) do|loc| + options[:loc] = loc + end + + opts.on( '-p', '--port ', 'port number' ) do|port| + options[:port] = port + end + + opts.on( '-w', '--passwd ', 'password for package server' ) do|passwd| + options[:passwd] = passwd + end + + opts.on( '--clone', 'clone mode' ) do + options[:clone] = true + end + + opts.on( '--force', 'force update pkg file' ) do + options[:force] = true + end + + opts.on( '--test', 'upload for test' ) do + options[:test] = true + end + + opts.on( '--gen', 'generate snapshot' ) do + options[:gensnap] = true + end + + opts.on( '-h', '--help', 'display help' ) do + puts opts exit - end - - end - - cmd = ARGV[0] - - if cmd.eql? "create" or cmd.eql? "register" or cmd.eql? "sync" \ - or cmd.eql? "gen-snapshot" or cmd.eql? "add-dist" \ - or cmd.eql? "spkg-path" or cmd.eql? "remove" \ - or cmd.eql? "list" or cmd.eql? "remove-pkg" or cmd =~ /(help)|(-h)|(--help)/ then - if cmd.eql? "help" then ARGV[0] = "-h" end - options[:cmd] = ARGV[0] - else - raise ArgumentError, banner - end - - optparse.parse! - - # default value setting + end + + opts.on( '-v', '--version', 'display version' ) do + puts "DIBS(Distributed Intelligent Build System) version " + Utils.get_version() + exit + end + end + + cmd = ARGV[0] + + if cmd.eql? "create" or cmd.eql? "sync" \ + or cmd.eql? "register" \ + or cmd.eql? "gen-snapshot" \ + or cmd.eql? "add-dist" or cmd.eql? "add-os" \ + or cmd.eql? "remove" or cmd.eql? "remove-dist" \ + or cmd.eql? "remove-pkg" or cmd.eql? "remove-snapshot" \ + or cmd.eql? "start" or cmd.eql? "stop" or cmd.eql? "clean" \ + or cmd.eql? "list" \ + or cmd =~ /(-v)|(--version)/ \ + or cmd =~ /(help)|(-h)|(--help)/ then + if cmd.eql? "help" then ARGV[0] = "-h" end + options[:cmd] = ARGV[0] + else + raise ArgumentError, "Usage: pkg-svr [OPTS] or pkg-svr -h" + end + + # default value setting set_default options - # option error check + optparse.parse! + + # option error check option_error_check options - return options -end + return options +end diff --git a/test/bin/bin_0.0.0_linux.zip b/test/bin/bin_0.0.0_linux.zip new file mode 100644 index 0000000000000000000000000000000000000000..9880da1e42ac1986bbbe9bf1b83e51dfec0c1056 GIT binary patch literal 620 zcmWIWW@h1H00I5ZRSsYVlwf6$VMs|VNz@Mw;bdUmRleFEgi9;985mh!Ff%ZKi2xMs z>yUv!>vv06zJ%s%PS>MrH8|cae|y0vT78nwfqVD1 zgo;m1d{AvayS#N*yVzI1@FTDK6em4jvrTROt~*A2M_w-N>ug(AVCVJl@p)V3jZcnG z&}Dohl+9kI_&7oSp`h^gW#9UX7F#cJ+JZycwC~m~jQQ1kleQAi(g}5kx}+o)r@C zXdXp33N_dvMghZ*VM(Js%qUWhFp9!3TR0iX#5+3A^iY597& ziFui6sl_E=vreb-27oY{TT=T^a~(1eaQ*(XtEPc>ku|65(X|>J@0P!vu%y3JWwPu4 zdt0uyPECApX3p-NM-9a(wf{SnKZ`!v_|$Ncz_ zyzW8A=cE()Y&pW7+*OLH4*0tmRcCHhpYZ8X));HK4!H;Tt8G&`TX1a0p5&Ea?H4bTLS2B5D;K^>jFfuR*08J>!PS4Cs%h$_I z%*#wmEiM6@6_|R~AB54|lG^Xcb;v-#^}D4jUxM>pFAc3cSH}YTTxX-)xgJK0)zz27 zVy_%MF#G?%XYmVP9+cWS*>uv5*#Z}X|9OS3TD#+p5ue9P)jrPlZ3T8k4G7CL7 zR&?iC=B*<8qIF7hZHv24O6X(xt8Qf-%_WP^>)LLdRT+9|t-5t*T(3h}YIRlr{nTroteN2~b37q4T!(>-E^Mu-`J->JZycwC~m~jQR1kl$YAi(g}5ky0So)r@G zXkJA(3N_#%Mgc>RVM(Js%qUO2t{-Fun*KyCkO?9{^&AWxVS*XfqWu~PTmw?T>0CWckqnVZ3Z_RheK)|JX{vmdSum-+JL4_z5A)&u&k(TGu zd*75!y!}`uK%i^ty*IPd-UXB<1jwaidfHrMf32}J=4qC8UVqERo+F00-ygBOweOsi zxJB{9<1q(?mOIbC$8*DN67P%F5a#0y&K)TsPur4|qqD1T_ULW>awO|nsOR;*Gb&c5 zOf#p>Rb4b^%D-j%7wH`7ym{N`Q@Zkxz!-y&d;2P8o|34D`_C5O&B!Fjj4Qw;fIbHS z0fx7ZAQ~F{tdQVG^DeqksDTeL3K)tEOB(HAMggM&pINw~0b$mb#zvr7n2`cB6-$H! Uc(bwrl`}8{;aeb`4{{Cz0P`WK*#H0l literal 0 HcmV?d00001 diff --git a/test/bin/src.tar.gz b/test/bin/src.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c41dcd82625abf1d4d8b98322237d81743f6ef37 GIT binary patch literal 150 zcmb2|=3qFVe%7CX`RzqVE+#{s)`x3sUGo#HbA{IjAHCaPQ2OqDh=5B0%fgN`|8qBa zDDcP4^{ak)MqlT-S##=t8+jF$%N7||B#yRE2o{-M&7S?NN_TQ`k<9atH5(7_-tGU} xK6Ls&@8|Ms6MQq5{(rc&-cc`f6c;x4EB{WH#7ScFlaC^003{mKPCVG literal 0 HcmV?d00001 diff --git a/test/build-server.basic1/build-cli-01.testcase b/test/build-server.basic1/build-cli-01.testcase new file mode 100644 index 0000000..90e5080 --- /dev/null +++ b/test/build-server.basic1/build-cli-01.testcase @@ -0,0 +1,43 @@ +#PRE-EXEC +#EXEC +../../build-cli -h +#POST-EXEC +#EXPECT +Requiest service to build-server command-line tool. + +Usage: build-cli [OPTS] or build-cli (-h|-v) + +Subcommands: +build Build and create package. +resolve Request change to resolve-status for build-conflict. +query Query information about build-server. +query-system Query system information about build-server. +query-project Query project information about build-server. +query-job Query job information about build-server. +cancel Cancel a building project. +register Register the package to the build-server. + +Subcommand usage: +build-cli build -N -d [-o ] [-w ] [--async] [-D ] [-U user-email] [-V] +build-cli resolve -N -d [-o ] [-w ] [--async] [-D ] [-U user-email] [-V] +build-cli query -d +build-cli query-system -d +build-cli query-project -d +build-cli query-job -d +build-cli cancel -j -d [-w ] +build-cli register -P -d [-t ] [-w ] [-D ] [-U user-email] + +Options: +-N, --project project name +-d, --address build server address: 127.0.0.1:2224 +-o, --os target operating system: ubuntu-32/ubuntu-64/windows-32/windows-64/macos-64 +--async asynchronous job +-j, --job job number +-w, --passwd password for managing project +-P, --pkg package file path +-D, --dist distribution name +-t, --ftp ftp server url: ftp://dibsftp:dibsftp@127.0.0.1 +-U, --user user email infomation +-V, --verbose verbose mode +-h, --help display help +-v, --version display version diff --git a/test/build-server.basic1/build-cli-02.testcase b/test/build-server.basic1/build-cli-02.testcase new file mode 100644 index 0000000..4d821bd --- /dev/null +++ b/test/build-server.basic1/build-cli-02.testcase @@ -0,0 +1,29 @@ +#PRE-EXEC +#EXEC +../../build-cli query -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* SYSTEM INFO * +HOST-OS: ubuntu-32 +MAX_WORKING_JOBS: 2 + +* FTP * +FTP_ADDR: +FTP_USERNAME: + +* SUPPORTED OS LIST * +ubuntu-32 +windows-32 + +* FRIEND SERVER LIST (WAIT|WORK/MAX) jobs [transfer count] * + + +* PROJECT(S) * +testa NORMAL +testa1 NORMAL +testb NORMAL +testc NORMAL +testd NORMAL +teste REMOTE + +* JOB(S) * diff --git a/test/build-server.basic1/build-cli-03.testcase b/test/build-server.basic1/build-cli-03.testcase new file mode 100644 index 0000000..b084d40 --- /dev/null +++ b/test/build-server.basic1/build-cli-03.testcase @@ -0,0 +1,30 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-server.basic1/build-cli-03_1.testcase b/test/build-server.basic1/build-cli-03_1.testcase new file mode 100644 index 0000000..5324ae3 --- /dev/null +++ b/test/build-server.basic1/build-cli-03_1.testcase @@ -0,0 +1,28 @@ +#PRE-EXEC +echo "This is the test case for omitting os" +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.basic1/build-cli-04.testcase b/test/build-server.basic1/build-cli-04.testcase new file mode 100644 index 0000000..977d486 --- /dev/null +++ b/test/build-server.basic1/build-cli-04.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N non_exist_project -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Project not found!: non_exist_project on BASE diff --git a/test/build-server.basic1/build-cli-05.testcase b/test/build-server.basic1/build-cli-05.testcase new file mode 100644 index 0000000..0072e13 --- /dev/null +++ b/test/build-server.basic1/build-cli-05.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testa -d 127.0.0.1:11113 -o ubuntu-32 +#POST-EXEC +#EXPECT +Connection to server failed! diff --git a/test/build-server.basic1/build-cli-06.testcase b/test/build-server.basic1/build-cli-06.testcase new file mode 100644 index 0000000..1682338 --- /dev/null +++ b/test/build-server.basic1/build-cli-06.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testa -d 111.11q.111.111:1111 -o ubuntu-32 +#POST-EXEC +#EXPECT +Connection to server failed! diff --git a/test/build-server.basic1/build-cli-07.testcase b/test/build-server.basic1/build-cli-07.testcase new file mode 100644 index 0000000..10e64d4 --- /dev/null +++ b/test/build-server.basic1/build-cli-07.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "testa project is already built and uploaded in previeous testcase" +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Error: Version must be increased : +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-08.testcase b/test/build-server.basic1/build-cli-08.testcase new file mode 100644 index 0000000..6d08be1 --- /dev/null +++ b/test/build-server.basic1/build-cli-08.testcase @@ -0,0 +1,32 @@ +#PRE-EXEC +echo "Assume testa project is already built and uploaded in previeous testcase" +#EXEC +../../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-server.basic1/build-cli-09.testcase b/test/build-server.basic1/build-cli-09.testcase new file mode 100644 index 0000000..b20cb5b --- /dev/null +++ b/test/build-server.basic1/build-cli-09.testcase @@ -0,0 +1,19 @@ +#PRE-EXEC +echo "if build-dep package does not exist in server, will show the error" +echo "Assume testa/testb project is already built and uploaded in previeous testcase" +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P b +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Error: Unmet dependency found! +Error: * a(ubuntu-32) for build-dependency +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-10.testcase b/test/build-server.basic1/build-cli-10.testcase new file mode 100644 index 0000000..c585766 --- /dev/null +++ b/test/build-server.basic1/build-cli-10.testcase @@ -0,0 +1,32 @@ +#PRE-EXEC +echo "This is the test case for omitting os" +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-server.basic1/build-cli-11.testcase b/test/build-server.basic1/build-cli-11.testcase new file mode 100644 index 0000000..beb3e82 --- /dev/null +++ b/test/build-server.basic1/build-cli-11.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "if there doe not exist server to build, error" +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o windows-32 +#POST-EXEC +#EXPECT +Info: Added new job "5" for windows-32! +Info: Initializing job... +Error: No servers that are able to build your packages. +Error: Host-OS (windows-32) is not supported in build server. +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-12.testcase b/test/build-server.basic1/build-cli-12.testcase new file mode 100644 index 0000000..a0b9e6f --- /dev/null +++ b/test/build-server.basic1/build-cli-12.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +echo "wrong os name in build command" +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o wrong_os_name +#POST-EXEC +#EXPECT +Error: Unsupported OS name used!: wrong_os_name +Supported OS list. +* ubuntu-32 +* windows-32 diff --git a/test/build-server.basic1/build-cli-12_1.testcase b/test/build-server.basic1/build-cli-12_1.testcase new file mode 100644 index 0000000..cccfe9f --- /dev/null +++ b/test/build-server.basic1/build-cli-12_1.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +echo "wrong os name in resolve command" +#EXEC +../../build-cli resolve -N testa -d 127.0.0.1:2223 -o wrong_os_name +#POST-EXEC +#EXPECT +Error: Unsupported OS name used!: wrong_os_name +Supported OS list. +* ubuntu-32 +* windows-32 diff --git a/test/build-server.basic1/build-cli-13.testcase b/test/build-server.basic1/build-cli-13.testcase new file mode 100644 index 0000000..59fd045 --- /dev/null +++ b/test/build-server.basic1/build-cli-13.testcase @@ -0,0 +1,38 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +echo "For, work around solution, removed cache" +rm -rf buildsvr01/projects/testa/cache +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +../../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -w 1111 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: * b +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... c_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testc" diff --git a/test/build-server.basic1/build-cli-14.testcase b/test/build-server.basic1/build-cli-14.testcase new file mode 100644 index 0000000..df9fff2 --- /dev/null +++ b/test/build-server.basic1/build-cli-14.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Project password required!: Use -w option to input your project password diff --git a/test/build-server.basic1/build-cli-15.testcase b/test/build-server.basic1/build-cli-15.testcase new file mode 100644 index 0000000..9b7cd98 --- /dev/null +++ b/test/build-server.basic1/build-cli-15.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +echo "Assume that testc project has the password (1111)" +echo "Assume that testa,testb which are depended by testc are built and uploaded" +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -w 2222 -o ubuntu-32 +#POST-EXEC +#EXPECT +Error: Project password not matched! diff --git a/test/build-server.basic1/build-cli-16.testcase b/test/build-server.basic1/build-cli-16.testcase new file mode 100644 index 0000000..1f798aa --- /dev/null +++ b/test/build-server.basic1/build-cli-16.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P c +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -w 1111 --async -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Above job(s) will be processed asynchronously! diff --git a/test/build-server.basic1/build-cli-17.testcase b/test/build-server.basic1/build-cli-17.testcase new file mode 100644 index 0000000..9653e45 --- /dev/null +++ b/test/build-server.basic1/build-cli-17.testcase @@ -0,0 +1,40 @@ +#PRE-EXEC +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P c +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P b +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable -P a +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 --async -o ubuntu-32 +sleep 1 +../../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Above job(s) will be processed asynchronously! +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Waiting for finishing following jobs: +Info: * +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-server.basic1/build-cli-18.testcase b/test/build-server.basic1/build-cli-18.testcase new file mode 100644 index 0000000..32218a2 --- /dev/null +++ b/test/build-server.basic1/build-cli-18.testcase @@ -0,0 +1,31 @@ +#PRE-EXEC +echo "reverse fail" +#EXEC +rm -rf git01/a +cd git01;tar xf a_v2.tar.gz +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.2_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: * Will check reverse-build for projects: testb(ubuntu-32) +Info: * Added new job for reverse-build ... testb(ubuntu-32) +Info: * Reverse-build FAIL ... testb(ubuntu-32) +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-19.testcase b/test/build-server.basic1/build-cli-19.testcase new file mode 100644 index 0000000..076004e --- /dev/null +++ b/test/build-server.basic1/build-cli-19.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +#EXEC +../../build-cli query-system -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* SYSTEM INFO * +HOST-OS: +MAX_WORKING_JOBS: + +* FTP * +FTP_ADDR: +FTP_USERNAME: + +* SUPPORTED OS LIST * +ubuntu-32 +windows-32 diff --git a/test/build-server.basic1/build-cli-20.testcase b/test/build-server.basic1/build-cli-20.testcase new file mode 100644 index 0000000..e475cf8 --- /dev/null +++ b/test/build-server.basic1/build-cli-20.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-cli query-project -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* PROJECT(S) * diff --git a/test/build-server.basic1/build-cli-21.testcase b/test/build-server.basic1/build-cli-21.testcase new file mode 100644 index 0000000..5de4383 --- /dev/null +++ b/test/build-server.basic1/build-cli-21.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-cli query-job -d 127.0.0.1:2223 +#POST-EXEC +#EXPECT +* JOB(S) * diff --git a/test/build-server.basic1/build-cli-22.testcase b/test/build-server.basic1/build-cli-22.testcase new file mode 100644 index 0000000..9ee83c9 --- /dev/null +++ b/test/build-server.basic1/build-cli-22.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +echo "Trying to upload a_0.0.1 with different commit-id is already uploaded" +rm -rf git01/c +cd git01;tar xf c_v1_1.tar.gz +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Error: Source code has been changed without increasing version! +Error: * Version : +Error: * Before : +Error: * Current : +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-23.testcase b/test/build-server.basic1/build-cli-23.testcase new file mode 100644 index 0000000..44ff734 --- /dev/null +++ b/test/build-server.basic1/build-cli-23.testcase @@ -0,0 +1,25 @@ +#PRE-EXEC +cd git01;tar xf a_v2.tar.gz +cd git01;tar xf b_v2.tar.gz +cd git01;tar xf c_v2.tar.gz +#EXEC +../../build-cli build -N testa,testb,testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job "testa" for ubuntu-32! +Info: Added new job "testb" for ubuntu-32! +Info: Added new job "testc" for ubuntu-32! +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.basic1/build-cli-24.testcase b/test/build-server.basic1/build-cli-24.testcase new file mode 100644 index 0000000..de1b348 --- /dev/null +++ b/test/build-server.basic1/build-cli-24.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +echo "This test case must be execute right after testcase 22" +#EXEC +../../build-cli build -N testa,testb,testc -d 127.0.0.1:2223 -o ubuntu-32 -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Failed to initialize sub-job +Info: Failed to initialize sub-job +Info: Failed to initialize sub-job +Error: Job is stopped by ERROR diff --git a/test/build-server.basic1/build-cli-25.testcase b/test/build-server.basic1/build-cli-25.testcase new file mode 100644 index 0000000..adaeb98 --- /dev/null +++ b/test/build-server.basic1/build-cli-25.testcase @@ -0,0 +1,20 @@ +#PRE-EXEC +cd git01;tar xf a_v3.tar.gz +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32,windows-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job "testa" for ubuntu-32! +Info: Added new job "testa" for windows-32! +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.basic1/build-cli-26.testcase b/test/build-server.basic1/build-cli-26.testcase new file mode 100644 index 0000000..eb0965c --- /dev/null +++ b/test/build-server.basic1/build-cli-26.testcase @@ -0,0 +1,45 @@ +#PRE-EXEC +echo "testa, testb: build because of version change" +echo "testc, testa1, testd: rebuild with same version" + +cd git01;tar xf a_v4.tar.gz +cd git01;tar xf b_v4.tar.gz +cd git01;tar xf c_v4.tar.gz +#EXEC +../../build-svr fullbuild -n testserver3 +#POST-EXEC +#EXPECT +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: Added new job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: * Sub-Job +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.basic1/build-cli-27.testcase b/test/build-server.basic1/build-cli-27.testcase new file mode 100644 index 0000000..66eb1cf --- /dev/null +++ b/test/build-server.basic1/build-cli-27.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cd git01;tar xf c_v5.tar.gz +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -o li_* -w 1111 +#POST-EXEC +#EXPECT +Error: Unsupported OS name used!: There is no OS name matched. diff --git a/test/build-server.basic1/build-cli-28.testcase b/test/build-server.basic1/build-cli-28.testcase new file mode 100644 index 0000000..27dfb11 --- /dev/null +++ b/test/build-server.basic1/build-cli-28.testcase @@ -0,0 +1,33 @@ +#PRE-EXEC +echo "wild card" +#EXEC +../../build-cli build -N testc -d 127.0.0.1:2223 -o ubuntu-* -w 1111 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: * b +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... c_0.0.5_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testc" diff --git a/test/build-server.basic1/build-cli-29.testcase b/test/build-server.basic1/build-cli-29.testcase new file mode 100644 index 0000000..47d1580 --- /dev/null +++ b/test/build-server.basic1/build-cli-29.testcase @@ -0,0 +1,38 @@ +#PRE-EXEC +echo "reverse success" +#EXEC +rm -rf git01/a +cd git01;tar xf a_v5.tar.gz +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.5_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: * Will check reverse-build for projects: +Info: * Added new job for reverse-build ... +Info: * Added new job for reverse-build ... +Info: * Reverse-build OK ... +Info: * Reverse-build OK ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-server.basic1/build-cli-30.testcase b/test/build-server.basic1/build-cli-30.testcase new file mode 100644 index 0000000..2fe774e --- /dev/null +++ b/test/build-server.basic1/build-cli-30.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +echo "user check" +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 -U xxuser@user +#POST-EXEC +#EXPECT +Error: User account not found!: xxuser@user diff --git a/test/build-server.basic1/buildsvr.init b/test/build-server.basic1/buildsvr.init new file mode 100755 index 0000000..72cf1fe --- /dev/null +++ b/test/build-server.basic1/buildsvr.init @@ -0,0 +1,58 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi + +if [ ! "$DB_PASSWD" ] ; then + read -p "Insert DB password: " input + export DB_PASSWD=$input +else + echo $DB_PASSWD +fi + +rm -rf buildsvr01 +rm -rf git01 +rm -rf bin +rm -rf ~/.build_tools/build_server/testserver3 + +../../build-svr remove -n testserver3 +mkdir buildsvr01 +cd buildsvr01 +${RUBY} ../../../build-svr create -n testserver3 +echo "DROP DATABASE testserver3;" > a +mysql -u root -p --password=$DB_PASSWD -h localhost < a +${RUBY} ../../../build-svr migrate -n testserver3 --dsn Mysql:testserver3:localhost --dbuser root --dbpassword $DB_PASSWD +rm -f a +cd .. +${RUBY} ../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 +${RUBY} ../../build-svr add-svr -n testserver3 -d 127.0.0.1:2224 +${RUBY} ../../build-svr add-os -n testserver3 -o ubuntu-32 +${RUBY} ../../build-svr add-os -n testserver3 -o windows-32 +${RUBY} ../../build-svr add-prj -n testserver3 -N testa -g `pwd`/git01/a -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testb -g `pwd`/git01/b -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testc -g `pwd`/git01/c -b master -w 1111 +${RUBY} ../../build-svr add-prj -n testserver3 -N testd -g `pwd`/git01/d -b master -o ubuntu-32 +${RUBY} ../../build-svr add-prj -n testserver3 -N teste -P bin +${RUBY} ../../build-svr add-prj -n testserver3 -N testa1 -g `pwd`/git01/a1 -b master + +mkdir -p git01 +cp ../git01/*.tar.gz git01/ +cd git01 +rm -rf a +rm -rf a1 +rm -rf b +rm -rf c +rm -rf d +tar xvf a_v1.tar.gz +tar xvf b_v1.tar.gz +tar xvf c_v1.tar.gz +tar xvf d_v0.tar.gz +tar xvf a1_v1.tar.gz +cd .. + +mkdir -p bin +cp ../bin/* bin/ + +${RUBY} ../../pkg-svr register -n pkgsvr01 -d unstable -P bin/bin_0.0.0_ubuntu-32.zip + +${RUBY} ../../build-svr start -n testserver3 -p 2223 --CHILD diff --git a/test/build-server.basic1/pkgsvr.init b/test/build-server.basic1/pkgsvr.init new file mode 100755 index 0000000..6973d9e --- /dev/null +++ b/test/build-server.basic1/pkgsvr.init @@ -0,0 +1,10 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +${RUBY} ../../pkg-svr create -n pkgsvr01 -d unstable +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o windows-32 +${RUBY} ../../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/build-server.basic1/testsuite b/test/build-server.basic1/testsuite new file mode 100644 index 0000000..b5b5903 --- /dev/null +++ b/test/build-server.basic1/testsuite @@ -0,0 +1,31 @@ +build-cli-01.testcase +build-cli-02.testcase +build-cli-03.testcase +build-cli-04.testcase +build-cli-05.testcase +build-cli-06.testcase +build-cli-07.testcase +build-cli-08.testcase +build-cli-09.testcase +build-cli-10.testcase +build-cli-11.testcase +build-cli-12.testcase +build-cli-12_1.testcase +build-cli-13.testcase +build-cli-14.testcase +build-cli-15.testcase +build-cli-16.testcase +build-cli-17.testcase +build-cli-18.testcase +build-cli-19.testcase +build-cli-20.testcase +build-cli-21.testcase +build-cli-22.testcase +build-cli-23.testcase +build-cli-24.testcase +build-cli-25.testcase +build-cli-26.testcase +build-cli-27.testcase +build-cli-28.testcase +build-cli-29.testcase +build-cli-30.testcase diff --git a/test/build-server.basic2/build-svr-01.testcase b/test/build-server.basic2/build-svr-01.testcase new file mode 100644 index 0000000..f9617b1 --- /dev/null +++ b/test/build-server.basic2/build-svr-01.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf buildsvr01 +mkdir buildsvr01 +#EXEC +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://ftpuser:ftpuser@172.21.111.124 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Created new build server: "testserver3" diff --git a/test/build-server.basic2/build-svr-02.testcase b/test/build-server.basic2/build-svr-02.testcase new file mode 100644 index 0000000..271b3c0 --- /dev/null +++ b/test/build-server.basic2/build-svr-02.testcase @@ -0,0 +1,79 @@ +#PRE-EXEC +#EXEC +../../build-svr -h +#POST-EXEC +#EXPECT +Build-server administer service command-line tool. + +Usage: build-svr [OPTS] or build-svr (-h|-v) + +Subcommands: +create Create the build-server. +remove Remove the build-server. +migrate build-server DB migrate. +start Start the build-server. +stop Stop the build-server. +upgrade Upgrade the build-server include friends. +add-svr Add remote build/package server for support multi-OS or distribute build job. +remove-svr Remove remote build/package server for support multi-OS or distribute build job. +add-os Add supported OS. +remove-os Remove supported OS. +add-dist Add distribution. +remove-dist Remove distribution. +lock-dist Lock distribution. +unlock-dist Unlock distribution. +add-sync Add package repository URL to synchronize with. +remove-sync Remove package repository URL. +add-prj Add project to build. +remove-prj Remove project. +register Register the package to the build-server. +fullbuild Build all your projects and upload them to package server. +query Show build server configuration. +set-attr Set build server atribute. +get-attr Get build server atribute. + +Subcommand usage: +build-svr create -n [-t ] +build-svr remove -n +build-svr migrate -n [--dsn [--dbuser --dbpassword ] ] +build-svr start -n -p +build-svr stop -n +build-svr upgrade -n +build-svr add-svr -n -d +build-svr remove-svr -n -d +build-svr add-os -n -o +build-svr remove-os -n -o +build-svr add-dist -n -D -u -d +build-svr remove-dist -n -D +build-svr lock-dist -n -D +build-svr unlock-dist -n -D +build-svr add-sync -n -u [--dist ] +build-svr remove-sync -n -u [--dist ] +build-svr add-prj -n -N (-g -b |-P ) [-w ] [-o ] [--dist ] +build-svr remove-prj -n -N [--dist ] +build-svr fullbuild -n [--dist ] +build-svr register -n -P [--dist ] +build-svr query -n +build-svr set-attr -n -A -V +build-svr get-attr -n -A + +Options: +-n, --name build server name +-u, --url package server url: http://127.0.0.1/dibs/unstable +-d, --address server address: 127.0.0.1:2224 +-p, --port server port number: 2224 +-P, --pkg package file path or name +-o, --os ex) ubuntu-32,windows-32 +-N, --pname project name +-g, --git git repository +-b, --branch git branch +-D, --dist distribution name +-w, --passwd password for managing project +-t, --ftp ftp server url: ftp://dibsftp:dibsftp@127.0.0.1:1024 +-A, --attr attribute +--dsn Data Source Name ex) mysql:host=localhost;database=test +--dbuser DB user id +--dbpassword DB password +-V, --value value +-h, --help display this information +-v, --version display version diff --git a/test/build-server.basic2/build-svr-03.testcase b/test/build-server.basic2/build-svr-03.testcase new file mode 100644 index 0000000..236a9ef --- /dev/null +++ b/test/build-server.basic2/build-svr-03.testcase @@ -0,0 +1,24 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://dibsftp:coreps2@172.21.111.132 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +#EXEC +../../build-svr add-svr -n testserver3 -d 127.0.0.1:2223 +../../build-svr query -n testserver3 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Friend server is added successfully! +* REMOTE SERVER(S) * +* 127.0.0.1:2223 + +* SUPPORTED OS * + +* DISTRIBUTION(S) * +* BASE + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * diff --git a/test/build-server.basic2/build-svr-04.testcase b/test/build-server.basic2/build-svr-04.testcase new file mode 100644 index 0000000..985f511 --- /dev/null +++ b/test/build-server.basic2/build-svr-04.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +echo "TEST_TIME=3" >> ~/.build_tools/build_server/testserver3/server.cfg +../../build-svr start -n testserver3 -p 2223 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT diff --git a/test/build-server.basic2/build-svr-05.testcase b/test/build-server.basic2/build-svr-05.testcase new file mode 100644 index 0000000..23d530f --- /dev/null +++ b/test/build-server.basic2/build-svr-05.testcase @@ -0,0 +1,15 @@ +#PRE-EXEC +mkdir buildsvr01 +rm -rf ~/.build_tools/build_server/testserver3 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://dibsftp:coreps2@172.21.111.132 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +../../build-svr start -n testserver3 -p 2223 & +#EXEC +sleep 1 +../../build-svr stop -n testserver3 +sleep 1 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Server will be down! diff --git a/test/build-server.basic2/build-svr-06.testcase b/test/build-server.basic2/build-svr-06.testcase new file mode 100644 index 0000000..b3f62fe --- /dev/null +++ b/test/build-server.basic2/build-svr-06.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../../build-svr stop -n testserver3 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +generating +Server is not running! diff --git a/test/build-server.basic2/build-svr-07.testcase b/test/build-server.basic2/build-svr-07.testcase new file mode 100644 index 0000000..b5518f5 --- /dev/null +++ b/test/build-server.basic2/build-svr-07.testcase @@ -0,0 +1,9 @@ +#PRE-EXEC +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../../build-svr remove -n testserver3 +#POST-EXEC +rm -rf buildsvr01 +#EXPECT +Removed the server diff --git a/test/build-server.basic2/build-svr-08.testcase b/test/build-server.basic2/build-svr-08.testcase new file mode 100644 index 0000000..3ab2171 --- /dev/null +++ b/test/build-server.basic2/build-svr-08.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Creating server failed. The server id is already exist diff --git a/test/build-server.basic2/build-svr-09.testcase b/test/build-server.basic2/build-svr-09.testcase new file mode 100644 index 0000000..2a301eb --- /dev/null +++ b/test/build-server.basic2/build-svr-09.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-svr remove -n testserverxx +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-server.basic2/build-svr-10.testcase b/test/build-server.basic2/build-svr-10.testcase new file mode 100644 index 0000000..1d3f863 --- /dev/null +++ b/test/build-server.basic2/build-svr-10.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-svr start -n testserverxx +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-server.basic2/build-svr-11.testcase b/test/build-server.basic2/build-svr-11.testcase new file mode 100644 index 0000000..0b2f399 --- /dev/null +++ b/test/build-server.basic2/build-svr-11.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://dibsftp:coreps2@172.21.111.132 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +../../build-svr add-os -n testserver3 -o ubuntu-32 +#EXEC +../../build-svr add-prj -n testserver3 -N testa -g test_git -b test_branch +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Adding project succeeded! diff --git a/test/build-server.basic2/build-svr-12.testcase b/test/build-server.basic2/build-svr-12.testcase new file mode 100644 index 0000000..1dcbb18 --- /dev/null +++ b/test/build-server.basic2/build-svr-12.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-svr add-prj -n testserverxxx -N testa -g test_git -b test_branch +#POST-EXEC +#EXPECT +does not exist! diff --git a/test/build-server.basic2/build-svr-13.testcase b/test/build-server.basic2/build-svr-13.testcase new file mode 100644 index 0000000..f87355b --- /dev/null +++ b/test/build-server.basic2/build-svr-13.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://dibsftp:coreps2@172.21.111.132 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +#EXEC +../../build-svr add-prj -n testserver3 -N testa -g test_git -b test_branch -w 1111 +#POST-EXEC +#EXPECT +Adding project succeeded! diff --git a/test/build-server.basic2/build-svr-14.testcase b/test/build-server.basic2/build-svr-14.testcase new file mode 100644 index 0000000..ce59935 --- /dev/null +++ b/test/build-server.basic2/build-svr-14.testcase @@ -0,0 +1,14 @@ +#PRE-EXEC +rm -rf buildsvr01 +rm -rf ~/.build_tools/build_server/testserver3 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@127.0.0.1 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +../../build-svr add-os -n testserver3 -o ubuntu-32 +#EXEC +../../build-svr add-prj -n testserver3 -N testx -g test_git -b test_branch -o ubuntu-32 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Adding project succeeded! diff --git a/test/build-server.basic2/build-svr-15.testcase b/test/build-server.basic2/build-svr-15.testcase new file mode 100644 index 0000000..734b5a7 --- /dev/null +++ b/test/build-server.basic2/build-svr-15.testcase @@ -0,0 +1,19 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../../build-svr register -n testserver3 -P bin/bin_0.0.0_ubuntu-32.zip +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for REGISTER Job +Info: New Job +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.basic2/build-svr-16.testcase b/test/build-server.basic2/build-svr-16.testcase new file mode 100644 index 0000000..b5fe4fc --- /dev/null +++ b/test/build-server.basic2/build-svr-16.testcase @@ -0,0 +1,15 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../../build-svr register -n testserver3 -P bin/bin_0.0.0_ubuntu-32.zip +#EXEC +../../build-svr register -n testserver3 -P bin/bin_0.0.0_ubuntu-32.zip +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Info: Initializing job... +Info: Checking package version ... +Error: Version must be increased : +Error: Job is stopped by ERROR diff --git a/test/build-server.basic2/build-svr-17.testcase b/test/build-server.basic2/build-svr-17.testcase new file mode 100644 index 0000000..244ce63 --- /dev/null +++ b/test/build-server.basic2/build-svr-17.testcase @@ -0,0 +1,24 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://dibsftp:coreps2@172.21.111.132 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +#EXEC +../../build-svr add-os -n testserver3 -o ubuntu-32 +../../build-svr query -n testserver3 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Target OS is added successfully! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* BASE + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * diff --git a/test/build-server.basic2/build-svr-18.testcase b/test/build-server.basic2/build-svr-18.testcase new file mode 100644 index 0000000..d2517ee --- /dev/null +++ b/test/build-server.basic2/build-svr-18.testcase @@ -0,0 +1,14 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +#EXEC +../../build-svr add-os -n testserver3 -o ubuntu-32 +../../build-svr add-os -n testserver3 -o ubuntu-32 +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +generating +Target OS is added successfully! +Target OS already exists in list! diff --git a/test/build-server.basic2/build-svr-19.testcase b/test/build-server.basic2/build-svr-19.testcase new file mode 100644 index 0000000..5a7ce72 --- /dev/null +++ b/test/build-server.basic2/build-svr-19.testcase @@ -0,0 +1,16 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://dibsftp:coreps2@172.21.111.132 +../../build-svr add-os -n testserver3 -o ubuntu-32 +../../build-svr add-os -n testserver3 -o windows-32 +#EXEC +../../build-svr add-prj -n testserver3 -N new_project -g new_git -b new_branch -o wrong_os_name +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +Unsupported OS name "wrong_os_name" is used! +Check the following supported OS list: + * ubuntu-32 + * windows-32 diff --git a/test/build-server.basic2/build-svr-20.testcase b/test/build-server.basic2/build-svr-20.testcase new file mode 100644 index 0000000..1396b9c --- /dev/null +++ b/test/build-server.basic2/build-svr-20.testcase @@ -0,0 +1,22 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@172.21.111.124 +cd buildsvr01; ../../../build-svr add-dist -n testserver3 -D BASE -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +../../build-svr add-os -n testserver3 -o ubuntu-32 +mkdir -p bin +cp ../bin/bin_0.0.0_ubuntu-32.zip bin/bin_0.0.0_wrongosname.zip +../../build-svr start -n testserver3 -p 2223 & +#EXEC +sleep 1 +../../build-svr register -n testserver3 -P bin/bin_0.0.0_wrongosname.zip +#POST-EXEC +../../build-svr stop -n testserver3 +sleep 1 +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +rm -rf bin/bin/bin_0.0.0_wrongosname.zip +#EXPECT +Info: Initializing job... +Error: Unsupported OS "wrongosname" is used! +Error: Job is stopped by ERROR diff --git a/test/build-server.basic2/build-svr-21.testcase b/test/build-server.basic2/build-svr-21.testcase new file mode 100644 index 0000000..b703113 --- /dev/null +++ b/test/build-server.basic2/build-svr-21.testcase @@ -0,0 +1,20 @@ +#PRE-EXEC +rm -rf buildsvr01 +mkdir buildsvr01 +cd buildsvr01; ../../../build-svr create -n testserver3 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 -t ftp://ftpuser:ftpuser@172.21.111.124 +cd buildsvr01; ../../../build-svr migrate -n testserver3 +#EXEC +../../build-svr set-attr -n testserver3 -A MAX_WORKING_JOBS -V 3 +../../build-svr get-attr -n testserver3 -A MAX_WORKING_JOBS +../../build-svr set-attr -n testserver3 -A XXX +../../build-svr set-attr -n testserver3 -A XXX -V 1 +../../build-svr get-attr -n testserver3 -A XXX + +#POST-EXEC +../../build-svr remove -n testserver3 +rm -rf buildsvr01 +#EXPECT +3 +Usage: build-svr set-attr -n -A -V +Wrong attribute name! +Wrong attribute name! diff --git a/test/build-server.basic2/testsuite b/test/build-server.basic2/testsuite new file mode 100644 index 0000000..05f65d2 --- /dev/null +++ b/test/build-server.basic2/testsuite @@ -0,0 +1,19 @@ +build-svr-01.testcase +build-svr-02.testcase +build-svr-03.testcase +build-svr-04.testcase +build-svr-05.testcase +build-svr-06.testcase +build-svr-07.testcase +build-svr-08.testcase +build-svr-09.testcase +build-svr-10.testcase +build-svr-11.testcase +build-svr-12.testcase +build-svr-13.testcase +build-svr-14.testcase +build-svr-17.testcase +build-svr-18.testcase +build-svr-19.testcase +build-svr-20.testcase +build-svr-21.testcase diff --git a/test/build-server.multi-svr1/01.testcase b/test/build-server.multi-svr1/01.testcase new file mode 100644 index 0000000..21843e1 --- /dev/null +++ b/test/build-server.multi-svr1/01.testcase @@ -0,0 +1,45 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -o ubuntu-32 -D unstable +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Started to build this job... +Info: JobBuilder# +Info: Start to build on remote server... +Info: Sending build request to remote server... +Info: Added new job +Info: Initializing job... +Info: Copying external dependent pkgs... +Info: Invoking a thread for building Job +Info: New Job +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Copying log to +Info: Copying result files to +Info: * +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "UNNAMED +Info: Receiving log file from remote server... +Info: Receiving file from remote server : a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-server.multi-svr1/02.testcase b/test/build-server.multi-svr1/02.testcase new file mode 100644 index 0000000..cbe7440 --- /dev/null +++ b/test/build-server.multi-svr1/02.testcase @@ -0,0 +1,46 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testb -d 127.0.0.1:2223 -o ubuntu-32 -D unstable +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Started to build this job... +Info: JobBuilder +Info: Start to build on remote server... +Info: Sending build request to remote server... +Info: Added new job +Info: Initializing job... +Info: Copying external dependent pkgs... +Info: Invoking a thread for building Job +Info: New Job +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Copying log to +Info: Copying result files to +Info: * +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "UNNAMED +Info: Receiving log file from remote server... +Info: Receiving file from remote server : b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-server.multi-svr1/buildsvr1.init b/test/build-server.multi-svr1/buildsvr1.init new file mode 100755 index 0000000..9b0af28 --- /dev/null +++ b/test/build-server.multi-svr1/buildsvr1.init @@ -0,0 +1,46 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi + +if [ ! "$DB_PASSWD" ] ; then + echo -n "insert Mysql password : " + read input + export DB_PASSWD=$input +else + echo $DB_PASSWD +fi + +rm -rf buildsvr01 +rm -rf git01 +rm -rf bin +rm -rf ~/.build_tools/build_server/testserver3 + +../../build-svr remove -n testserver3 +mkdir buildsvr01 +cd buildsvr01 +${RUBY} ../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@127.0.0.1 +echo "DROP DATABASE testserver3;" > a +mysql -u root -p --password=$DB_PASSWD -h localhost < a +${RUBY} ../../../build-svr migrate -n testserver3 --dsn Mysql:testserver3:localhost --dbuser root --dbpassword $DB_PASSWD +rm -f a +cd .. + +${RUBY} ../../build-svr add-dist -n testserver3 -D unstable -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 +${RUBY} ../../build-svr add-svr -n testserver3 -d 127.0.0.1:2224 +${RUBY} ../../build-svr add-os -n testserver3 -o ubuntu-32 +${RUBY} ../../build-svr add-os -n testserver3 -o windows-32 +${RUBY} ../../build-svr add-prj -n testserver3 -N testa -g `pwd`/git01/a -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testb -g `pwd`/git01/b -b master +${RUBY} ../../build-svr set-attr -n testserver3 -A MAX_WORKING_JOBS -V 0 + +mkdir -p git01 +cp ../git01/*.tar.gz git01/ +cd git01 +rm -rf a +rm -rf b +tar xvf a_v1.tar.gz +tar xvf b_v1.tar.gz +cd .. + +${RUBY} ../../build-svr start -n testserver3 -p 2223 --CHILD diff --git a/test/build-server.multi-svr1/buildsvr2.init b/test/build-server.multi-svr1/buildsvr2.init new file mode 100755 index 0000000..56b8da5 --- /dev/null +++ b/test/build-server.multi-svr1/buildsvr2.init @@ -0,0 +1,26 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi + +if [ ! "$DB_PASSWD" ] ; then + read -p "Insert DB password: " input + export DB_PASSWD=$input +else + echo $DB_PASSWD +fi + +rm -rf buildsvr02 + +../../build-svr remove -n testserver4 +mkdir buildsvr02 +cd buildsvr02 +${RUBY} ../../../build-svr create -n testserver4 +echo "DROP DATABASE testserver4;" > a +mysql -u root -p --password=$DB_PASSWD -h localhost < a +${RUBY} ../../../build-svr migrate -n testserver4 --dsn Mysql:testserver4:localhost --dbuser root --dbpassword $DB_PASSWD +rm -f a +cd .. +${RUBY} ../../build-svr add-dist -n testserver4 -D unstable -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 + +${RUBY} ../../build-svr start -n testserver4 -p 2224 --CHILD diff --git a/test/build-server.multi-svr1/pkgsvr.init b/test/build-server.multi-svr1/pkgsvr.init new file mode 100755 index 0000000..0cc21e3 --- /dev/null +++ b/test/build-server.multi-svr1/pkgsvr.init @@ -0,0 +1,10 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +${RUBY} ../../pkg-svr create -n pkgsvr01 -d unstable +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o windows-32 +${RUBY} ../../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/build-server.multi-svr1/testsuite b/test/build-server.multi-svr1/testsuite new file mode 100644 index 0000000..960eff3 --- /dev/null +++ b/test/build-server.multi-svr1/testsuite @@ -0,0 +1,2 @@ +01.testcase +02.testcase diff --git a/test/build-server.multi-svr2/01.testcase b/test/build-server.multi-svr2/01.testcase new file mode 100644 index 0000000..f71a53f --- /dev/null +++ b/test/build-server.multi-svr2/01.testcase @@ -0,0 +1,23 @@ +#PRE-EXEC +#EXEC +../../pkg-svr register -n pkgsvr02 -d unstable -P bin/bin_0.0.1_ubuntu-32.zip +sleep 50 +../../pkg-cli list-rpkg -P bin -u `pwd`/pkgsvr01/unstable +#POST-EXEC +#EXPECT +Archive: bin/bin_0.0.1_ubuntu-32.zip +inflating: +snapshot is generated : +package registed successfully +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +bin (0.0.1) diff --git a/test/build-server.multi-svr2/buildsvr.init b/test/build-server.multi-svr2/buildsvr.init new file mode 100755 index 0000000..b0859c9 --- /dev/null +++ b/test/build-server.multi-svr2/buildsvr.init @@ -0,0 +1,37 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi + +if [ ! "$DB_PASSWD" ] ; then + read -p "Insert DB password: " input + export DB_PASSWD=$input +else + echo $DB_PASSWD +fi + +rm -rf buildsvr01 +rm -rf git01 +rm -rf bin +rm -rf ~/.build_tools/build_server/testserver3 + +../../build-svr remove -n testserver3 +mkdir buildsvr01 +cd buildsvr01 +${RUBY} ../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@127.0.0.1 +echo "DROP DATABASE testserver3;" > a +mysql -u root -p --password=$DB_PASSWD -h localhost < a +${RUBY} ../../../build-svr migrate -n testserver3 --dsn Mysql:testserver3:localhost --dbuser root --dbpassword $DB_PASSWD +rm -f a +cd .. + +${RUBY} ../../build-svr add-dist -n testserver3 -D unstable -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 +${RUBY} ../../build-svr add-os -n testserver3 -o ubuntu-32 +${RUBY} ../../build-svr add-os -n testserver3 -o windows-32 +${RUBY} ../../build-svr add-prj -n testserver3 -N teste -P bin -D unstable -o ubuntu-32 +${RUBY} ../../build-svr add-sync -n testserver3 -u `pwd`/pkgsvr02/unstable -D unstable + +mkdir -p bin +cp ../bin/*.zip ./bin/ + +${RUBY} ../../build-svr start -n testserver3 -p 2223 --CHILD diff --git a/test/build-server.multi-svr2/pkgsvr1.init b/test/build-server.multi-svr2/pkgsvr1.init new file mode 100755 index 0000000..0cc21e3 --- /dev/null +++ b/test/build-server.multi-svr2/pkgsvr1.init @@ -0,0 +1,10 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +${RUBY} ../../pkg-svr create -n pkgsvr01 -d unstable +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o windows-32 +${RUBY} ../../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/build-server.multi-svr2/pkgsvr2.init b/test/build-server.multi-svr2/pkgsvr2.init new file mode 100755 index 0000000..46ec0a7 --- /dev/null +++ b/test/build-server.multi-svr2/pkgsvr2.init @@ -0,0 +1,10 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr02 +rm -rf `pwd`/pkgsvr02 +${RUBY} ../../pkg-svr create -n pkgsvr02 -d unstable +${RUBY} ../../pkg-svr add-os -n pkgsvr02 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr02 -d unstable -o windows-32 +${RUBY} ../../pkg-svr start -n pkgsvr02 -p 3334 diff --git a/test/build-server.multi-svr2/testsuite b/test/build-server.multi-svr2/testsuite new file mode 100644 index 0000000..1bb2eb2 --- /dev/null +++ b/test/build-server.multi-svr2/testsuite @@ -0,0 +1 @@ +01.testcase diff --git a/test/build-server.multi_dist1/build-svr2-01.testcase b/test/build-server.multi_dist1/build-svr2-01.testcase new file mode 100644 index 0000000..fb3d879 --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-01.testcase @@ -0,0 +1,23 @@ +#PRE-EXEC +rm -rf buildsvr01 +rm -rf ~/.build_tools/build_server/testserver3 +mkdir buildsvr01 +cd buildsvr01;../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@127.0.0.1 +../../build-svr add-os -n testserver3 -o ubuntu-32 +../../build-svr add-os -n testserver3 -o ubuntu-64 +#EXEC +../../build-svr remove-os -n testserver3 -o ubuntu-64 +../../build-svr query -n testserver3 +#POST-EXEC +#EXPECT +Target OS is removed successfully! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * diff --git a/test/build-server.multi_dist1/build-svr2-02.testcase b/test/build-server.multi_dist1/build-svr2-02.testcase new file mode 100644 index 0000000..9298235 --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-02.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +echo "no os" +#EXEC +../../build-svr remove-os -n testserver3 -o ubuntu-644 +#POST-EXEC +#EXPECT +Target OS does not exist in list! diff --git a/test/build-server.multi_dist1/build-svr2-03.testcase b/test/build-server.multi_dist1/build-svr2-03.testcase new file mode 100644 index 0000000..06f96c2 --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-03.testcase @@ -0,0 +1,18 @@ +#PRE-EXEC +#EXEC +../../build-svr add-dist -n testserver3 -D unstable -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +../../build-svr query -n testserver3 +#POST-EXEC +#EXPECT +Distribution is added successfully! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * diff --git a/test/build-server.multi_dist1/build-svr2-04.testcase b/test/build-server.multi_dist1/build-svr2-04.testcase new file mode 100644 index 0000000..c667dfc --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-04.testcase @@ -0,0 +1,19 @@ +#PRE-EXEC +../../build-svr add-dist -n testserver3 -D unstable2 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +#EXEC +../../build-svr remove-dist -n testserver3 -D unstable2 +../../build-svr query -n testserver3 +#POST-EXEC +#EXPECT +Distribution is removed successfully! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * diff --git a/test/build-server.multi_dist1/build-svr2-05.testcase b/test/build-server.multi_dist1/build-svr2-05.testcase new file mode 100644 index 0000000..b08491d --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-05.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../../build-svr remove-dist -n testserver3 -D unstable22 +#POST-EXEC +#EXPECT +Distribution does not exist in list! diff --git a/test/build-server.multi_dist1/build-svr2-06.testcase b/test/build-server.multi_dist1/build-svr2-06.testcase new file mode 100644 index 0000000..685d52a --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-06.testcase @@ -0,0 +1,66 @@ +#PRE-EXEC +#EXEC +../../build-svr add-sync -n testserver3 -u http://xxx +../../build-svr query -n testserver3 +../../build-svr add-sync -n testserver3 -D unstable -u http://yyy +../../build-svr query -n testserver3 +../../build-svr remove-sync -n testserver3 -u http://yyy +../../build-svr query -n testserver3 +../../build-svr remove-sync -n testserver3 -D unstable testserver3 -u http://xxx +../../build-svr query -n testserver3 +../../build-svr remove-sync -n testserver3 -D unstable testserver3 -u http://xxxyyyy +#POST-EXEC +#EXPECT +Remote package server is added! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * +* [unstable] http://xxx + +* PROJECT(S) * +Remote package server is added! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * +* [unstable] http://xxx +* [unstable] http://yyy + +* PROJECT(S) * +Remote package server is removed! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * +* [unstable] http://xxx + +* PROJECT(S) * +Remote package server is removed! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +The server does not exist in list! diff --git a/test/build-server.multi_dist1/build-svr2-07.testcase b/test/build-server.multi_dist1/build-svr2-07.testcase new file mode 100644 index 0000000..6afd8c9 --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-07.testcase @@ -0,0 +1,78 @@ +#PRE-EXEC +../../build-svr add-dist -n testserver3 -D unstable2 -u `pwd`/../pkgsvr01/unstable -d 127.0.0.1:3333 +#EXEC +../../build-svr add-prj -n testserver3 -N test1 -g test1_git -b test1_branch +../../build-svr query -n testserver3 +../../build-svr add-prj -n testserver3 -D unstable -N test2 -g test1_git -b test1_branch +../../build-svr add-prj -n testserver3 -D unstable2 -N test1 -g test1_git -b test1_branch +../../build-svr query -n testserver3 +../../build-svr remove-prj -n testserver3 -N test1 +../../build-svr query -n testserver3 +../../build-svr remove-prj -n testserver3 -D unstable -N test2 +../../build-svr remove-prj -n testserver3 -D unstable2 -N test1 +../../build-svr query -n testserver3 +../../build-svr remove-prj -n testserver3 -D unstable -N testxxx +#POST-EXEC +../../build-svr remove-dist -n testserver3 -D unstable2 +#EXPECT +Adding project succeeded! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable +* unstable2 + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +* [unstable] test1 +Adding project succeeded! +Adding project succeeded! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable +* unstable2 + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +* [unstable] test1 +* [unstable] test2 +* [unstable2] test1 +Removing project succeeded! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable +* unstable2 + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +* [unstable] test2 +* [unstable2] test1 +Removing project succeeded! +Removing project succeeded! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable +* unstable2 + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +Removing project failed! diff --git a/test/build-server.multi_dist1/build-svr2-08.testcase b/test/build-server.multi_dist1/build-svr2-08.testcase new file mode 100644 index 0000000..90ef0b4 --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-08.testcase @@ -0,0 +1,21 @@ +#PRE-EXEC +#EXEC +../../build-svr add-prj -n testserver3 -D unstable -N testbin -P bin +../../build-svr query -n testserver3 +../../build-svr remove-prj -n testserver3 -N testbin -D unstable +#POST-EXEC +#EXPECT +Adding project succeeded! +* REMOTE SERVER(S) * + +* SUPPORTED OS * +* ubuntu-32 + +* DISTRIBUTION(S) * +* unstable + +* SYNC PACKAGE SERVER(S) * + +* PROJECT(S) * +* [unstable] testbin +Removing project succeeded! diff --git a/test/build-server.multi_dist1/build-svr2-09.testcase b/test/build-server.multi_dist1/build-svr2-09.testcase new file mode 100644 index 0000000..7c4a92e --- /dev/null +++ b/test/build-server.multi_dist1/build-svr2-09.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +#EXEC +../../build-svr lock-dist -n testserver3 -D unstable +../../build-svr lock-dist -n testserver3 -D unstable2 +../../build-svr unlock-dist -n testserver3 -D unstable +../../build-svr unlock-dist -n testserver3 -D unstable2 +#POST-EXEC +#EXPECT +Distribution is locked! +Locking distribution failed! +Distribution is unlocked! +Unlocking distribution failed! diff --git a/test/build-server.multi_dist1/testsuite b/test/build-server.multi_dist1/testsuite new file mode 100644 index 0000000..3f955a6 --- /dev/null +++ b/test/build-server.multi_dist1/testsuite @@ -0,0 +1,9 @@ +build-svr2-01.testcase +build-svr2-02.testcase +build-svr2-03.testcase +build-svr2-04.testcase +build-svr2-05.testcase +build-svr2-06.testcase +build-svr2-07.testcase +build-svr2-08.testcase +build-svr2-09.testcase diff --git a/test/build-server.multi_dist2/build-svr3-01.testcase b/test/build-server.multi_dist2/build-svr3-01.testcase new file mode 100644 index 0000000..224c40a --- /dev/null +++ b/test/build-server.multi_dist2/build-svr3-01.testcase @@ -0,0 +1,14 @@ +#PRE-EXEC +#EXEC +../../build-svr register -n testserver3 -D unstable -P bin/bin_0.0.0_ubuntu-32.zip +#POST-EXEC +#EXPECT +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for REGISTER Job +Info: New Job +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.multi_dist2/build-svr3-02.testcase b/test/build-server.multi_dist2/build-svr3-02.testcase new file mode 100644 index 0000000..b25a3de --- /dev/null +++ b/test/build-server.multi_dist2/build-svr3-02.testcase @@ -0,0 +1,56 @@ +#PRE-EXEC +#EXEC +../../build-cli build -N testa -d 127.0.0.1:2223 -D unstable +../../build-cli build -N testa -d 127.0.0.1:2223 -D unstable2 +#POST-EXEC +#EXPECT +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... a_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testa" diff --git a/test/build-server.multi_dist2/build-svr3-03.testcase b/test/build-server.multi_dist2/build-svr3-03.testcase new file mode 100644 index 0000000..be7df44 --- /dev/null +++ b/test/build-server.multi_dist2/build-svr3-03.testcase @@ -0,0 +1,28 @@ +#PRE-EXEC +#EXEC +../../build-cli register -d 127.0.0.1:2223 -P bin/bin_0.0.0_ubuntu-32.zip -D unstable2 -t ftp://ftpuser:ftpuser@127.0.0.1 +#POST-EXEC +#EXPECT +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +I, [ +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for REGISTER Job +Info: New Job +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! diff --git a/test/build-server.multi_dist2/build-svr3-04.testcase b/test/build-server.multi_dist2/build-svr3-04.testcase new file mode 100644 index 0000000..fe29a83 --- /dev/null +++ b/test/build-server.multi_dist2/build-svr3-04.testcase @@ -0,0 +1,18 @@ +#PRE-EXEC +#EXEC +../../build-svr fullbuild -n testserver3 -D unstable2 +#POST-EXEC +#EXPECT +Info: Initializing job... +Info: Invoking a thread for MULTI-BUILD Job +Info: New Job +Info: Added new job "testa" for ubuntu-32! +Info: Added new job "testb" for ubuntu-32! +Info: * Sub-Job "testa(ubuntu-32)" has entered "WORKING" state. +Info: * Sub-Job "testa(ubuntu-32)" has entered "FINISHED" state. +Info: * Sub-Job "testb(ubuntu-32)" has entered "WORKING" state. +Info: * Sub-Job "testb(ubuntu-32)" has entered "FINISHED" state. +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed diff --git a/test/build-server.multi_dist2/build-svr3-05.testcase b/test/build-server.multi_dist2/build-svr3-05.testcase new file mode 100644 index 0000000..6836515 --- /dev/null +++ b/test/build-server.multi_dist2/build-svr3-05.testcase @@ -0,0 +1,46 @@ +#PRE-EXEC +../../pkg-svr remove-pkg -n pkgsvr01 -d unstable2 -P b +#EXEC +echo "==" +../../build-svr lock-dist -n testserver3 -D unstable2 +echo "==" +../../build-cli build -N testb -d 127.0.0.1:2223 -D unstable2 +echo "==" +../../build-svr unlock-dist -n testserver3 -D unstable2 +echo "==" +../../build-cli build -N testb -d 127.0.0.1:2223 -D unstable2 +#POST-EXEC +#EXPECT +== +Distribution is locked! +== +Error: Distribution locked!: unstable2 +== +Distribution is unlocked! +== +Info: Added new job +Info: Initializing job... +Info: Checking package version ... +Info: Invoking a thread for building Job +Info: New Job +Info: Checking build dependency ... +Info: Checking install dependency ... +Info: Started to build this job... +Info: JobBuilder +Info: Downloding client is initializing... +Info: Installing dependent packages... +Info: * a +Info: Downloading dependent source packages... +Info: Make clean... +Info: Make build... +Info: Make install... +Info: Generatiing pkginfo.manifest... +Info: Zipping... +Info: Creating package file ... b_0.0.1_ubuntu-32.zip +Info: Checking reverse build dependency ... +Info: Uploading ... +Info: Upload succeeded. Sync local pkg-server again... +Info: Snapshot: +Info: Job is completed! +Info: Job is FINISHED successfully! +Info: Updating the source info for project "testb" diff --git a/test/build-server.multi_dist2/buildsvr.init b/test/build-server.multi_dist2/buildsvr.init new file mode 100755 index 0000000..5d4bc03 --- /dev/null +++ b/test/build-server.multi_dist2/buildsvr.init @@ -0,0 +1,50 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi + +if [ ! "$DB_PASSWD" ] ; then + read -p "Insert DB password: " input + export DB_PASSWD=$input +else + echo $DB_PASSWD +fi + +rm -rf buildsvr01 +rm -rf git01 +rm -rf bin +rm -rf ~/.build_tools/build_server/testserver3 + +../../build-svr remove -n testserver3 +mkdir buildsvr01 +cd buildsvr01 +${RUBY} ../../../build-svr create -n testserver3 -t ftp://ftpuser:ftpuser@127.0.0.1 +echo "DROP DATABASE testserver3;" > a +mysql -u root -p --password=$DB_PASSWD -h localhost < a +${RUBY} ../../../build-svr migrate -n testserver3 --dsn Mysql:testserver3:localhost --dbuser root --dbpassword $DB_PASSWD +rm -f a +cd .. + +${RUBY} ../../build-svr add-dist -n testserver3 -D unstable -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 +${RUBY} ../../build-svr add-dist -n testserver3 -D unstable2 -u `pwd`/pkgsvr01/unstable2 -d 127.0.0.1:3333 +${RUBY} ../../build-svr add-os -n testserver3 -o ubuntu-32 +${RUBY} ../../build-svr add-prj -n testserver3 -N testa -g `pwd`/git01/a -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testb -g `pwd`/git01/b -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testa -D unstable2 -g `pwd`/git01/a -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testb -D unstable2 -g `pwd`/git01/b -b master +${RUBY} ../../build-svr add-prj -n testserver3 -N testbin -D unstable -P bin +${RUBY} ../../build-svr add-prj -n testserver3 -N testbin -D unstable2 -P bin + +mkdir -p git01 +cp ../git01/*.tar.gz git01/ +cd git01 +rm -rf a +rm -rf b +tar xf a_v1.tar.gz +tar xf b_v1.tar.gz +cd .. + +mkdir -p bin +cp ../bin/* bin/ + +${RUBY} ../../build-svr start -n testserver3 -p 2223 --CHILD diff --git a/test/build-server.multi_dist2/pkgsvr.init b/test/build-server.multi_dist2/pkgsvr.init new file mode 100755 index 0000000..adfa33b --- /dev/null +++ b/test/build-server.multi_dist2/pkgsvr.init @@ -0,0 +1,13 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +${RUBY} ../../pkg-svr create -n pkgsvr01 -d unstable +${RUBY} ../../pkg-svr add-dist -n pkgsvr01 -d unstable2 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o windows-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable2 -o ubuntu-32 +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable2 -o windows-32 +${RUBY} ../../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/build-server.multi_dist2/testsuite b/test/build-server.multi_dist2/testsuite new file mode 100644 index 0000000..9f25506 --- /dev/null +++ b/test/build-server.multi_dist2/testsuite @@ -0,0 +1,5 @@ +build-svr3-01.testcase +build-svr3-02.testcase +build-svr3-03.testcase +build-svr3-04.testcase +build-svr3-05.testcase diff --git a/test/git01/a.tar.gz b/test/git01/a.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..be4b4ef3a53a61b24b8f2161af558b1acf77c3b8 GIT binary patch literal 14112 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 eH<$eXZM&1-|CyJ(}PK7y$rk?nSQv literal 0 HcmV?d00001 diff --git a/test/git01/a1_v1.tar.gz b/test/git01/a1_v1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b363c5aa0e951b66bf458bb34915e49852e0c354 GIT binary patch literal 9123 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 literal 0 HcmV?d00001 diff --git a/test/git01/a_v1.tar.gz b/test/git01/a_v1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..049b900a7fca767f934d156b12598b67b77d54e3 GIT binary patch literal 8555 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 I0fdO~KXEoHOaK4? literal 0 HcmV?d00001 diff --git a/test/git01/a_v4.tar.gz b/test/git01/a_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..25f7b3bb328ff58205e9479e02d386d178d1a18f GIT binary patch literal 10958 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 literal 0 HcmV?d00001 diff --git a/test/git01/b.tar.gz b/test/git01/b.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..6d263a1719f9ae22a8c9103fc94be121a467da1b GIT binary patch literal 13951 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

    3e~ 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 Lb`9W&;NkuU=IV+P literal 0 HcmV?d00001 diff --git a/test/git01/b_v4.tar.gz b/test/git01/b_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..60ae7c15a4d8f28f98ff893c541ad60cbd675882 GIT binary patch literal 11469 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 Pf?+=3|B?oY!hrq{&b+sw literal 0 HcmV?d00001 diff --git a/test/git01/c.tar.gz b/test/git01/c.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..92ddfb7edd851c38672de29a982aecf360162779 GIT binary patch literal 18834 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 Z1r$&~0RN)tcGMAJK2g7F)LYyK~xAu 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 R1O!AE{tXNfZ2kc7001I#6~zDm literal 0 HcmV?d00001 diff --git a/test/git01/c_v1_1.tar.gz b/test/git01/c_v1_1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..01177c7f367e75347d1f56fb4afa9e7741e858ff GIT binary patch literal 9680 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 literal 0 HcmV?d00001 diff --git a/test/git01/c_v4.tar.gz b/test/git01/c_v4.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..b108b4ae85abc39286ce617d8624e22e399ee9c6 GIT binary patch literal 11979 zcmX|lWmHsOxcAWAAfR;rkp^iT8bPGH8v*GqM{8^n}m;2te z-t%>z56^n`FE@QGHYi7*Km&w!Bx(GKWkI)nf#KJCTw+G_iI{ztrB6$%RX+OgS*Pi3bOWr9&YAB;@Us$%>)l8#7ETxmq&AL4Y%l3PE}#iLf% z?FF(aPWTLNR^DXy2!}D=uf?gZ9bDeYqHkP1x;8jfvYpYJ>Hpsmz5AY(U&7YWV|8^y z95ujeZF9f2_cNG8%gqO|R?%pO1ho2C04%$lvwg{1Y0Rdjk;3~m0gp%5nWbyg@^9>< zN^uEdK$}mw_Lgu{&aBFcqi{AsInJ=7*oxYcfRo;o%hwURrmFUj^7dP@ih2N9y)UCo zr&1K42Qz&1{ZH;<4UZ@CT4=Njh@2okH}~uVhNujY<~pQqK1dP0ga0>@>KTBV{H6X9 zuA99BWC+Hu11)SQ>^%Ucn-nF`bQm|wg@nU2C3Y;XBd44*%k`Fiji~jzw>p>GE_nBE zV!Adim0ugxTMJ3h+F^aJ9ut!gdZKIN1nZtp?OAye6Fr&D;9|ZS{wVaS(XL+G*w%Gg z]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 RfAb^&nq?BPA4nPn^gr^n&`|&Y literal 0 HcmV?d00001 diff --git a/test/git01/c_v5.tar.gz b/test/git01/c_v5.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..a6825814b41eef3d2068cc441e4ecfebfd778dd7 GIT binary patch literal 12861 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 literal 0 HcmV?d00001 diff --git a/test/git01/d.tar.gz b/test/git01/d.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..097b1f6339e11b144d8ec10f855f9801a6951ca5 GIT binary patch literal 15469 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 literal 0 HcmV?d00001 diff --git a/test/git01/d_v0.tar.gz b/test/git01/d_v0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..c0a3a6e7cab5984d3766f3bcfaf6350a71c3732f GIT binary patch literal 17463 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 GKmh [OPTS] or pkg-svr -h + +Subcommands: +create Create a package-server. +add-dist Add a distribution to package-server. +register Register a package in package-server. +remove Remove a package-server. +remove-dist Remove a distribution to package-server. +remove-snapshot Remove a snapshot in package-server. +gen-snapshot Generate a snapshot in package-server. +sync Synchronize the package-server from parent package server. +start Start the package-server. +stop Stop the package-server. +clean Delete unneeded package files in package-server. +list Show all pack + +Subcommand usage: +pkg-svr create -n -d [-u ] [-l ] +pkg-svr add-dist -n -d [-u ] [--clone] +pkg-svr add-os -n -d -o +pkg-svr register -n -d -P [--gen] [--test] +pkg-svr link -n -d --origin-pkg-name --origin-pkg-os --link-os-list +pkg-svr remove -n +pkg-svr remove-dist -n -d +pkg-svr remove-pkg -n -d -P [-o ] +pkg-svr remove-snapshot -n -d -s +pkg-svr gen-snapshot -n -d -s [-b ] +pkg-svr sync -n -d [--force] +pkg-svr clean -n -d [-s ] +pkg-svr start -n -p +pkg-svr stop -n -p +pkg-svr list [-n ] + +Options: +-n, --name package server name +-d, --dist package server distribution +-u, --url remote server url: http://127.0.0.1/dibs/unstable +-o, --os target operating system +-P, --pkgs package file path list +-s, --snapshot a snapshot name or snapshot list +-b, --base base snapshot name +-l, --loc server location +-p, --port port number + --clone clone mode + --force force update pkg file + --test upload for test + --gen generate snapshot + --origin-pkg-name + origin package name + --origin-pkg-os + origin package os + --link-os-list + target os list to link origin file +-h, --help display help +-v, --version display version diff --git a/test/packageserver02.testcase b/test/packageserver02.testcase new file mode 100644 index 0000000..408ca26 --- /dev/null +++ b/test/packageserver02.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +../pkg-svr remove -n temp_local --force +#EXEC +../pkg-svr create -n temp_local -d unstable +#POST-EXEC +#EXPECT +package server [temp_local] created successfully diff --git a/test/packageserver03.testcase b/test/packageserver03.testcase new file mode 100644 index 0000000..11eb774 --- /dev/null +++ b/test/packageserver03.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +../pkg-svr remove -n temp_remote --force +#EXEC +../pkg-svr create -n temp_remote -d unstable -u http://172.21.111.177/tmppkgsvr/tmp +#POST-EXEC +#EXPECT +snapshot is generated : +package server [temp_remote] created successfully diff --git a/test/packageserver04.testcase b/test/packageserver04.testcase new file mode 100644 index 0000000..3be6257 --- /dev/null +++ b/test/packageserver04.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +../pkg-svr remove -n temp_remote_dup --force +#EXEC +../pkg-svr create -n temp_remote_dup -d unstable -u temp_remote/unstable +#POST-EXEC +#EXPECT +package server [temp_remote_dup] created successfully diff --git a/test/packageserver05.testcase b/test/packageserver05.testcase new file mode 100644 index 0000000..da0ad18 --- /dev/null +++ b/test/packageserver05.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr add-dist -n temp_local -d stable +#POST-EXEC +#EXPECT +distribution [stable] added successfully diff --git a/test/packageserver06.testcase b/test/packageserver06.testcase new file mode 100644 index 0000000..cf49f46 --- /dev/null +++ b/test/packageserver06.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr sync -n temp_remote -d unstable +#POST-EXEC +#EXPECT +package server [temp_remote]'s distribution [unstable] has been synchronized. diff --git a/test/packageserver07.testcase b/test/packageserver07.testcase new file mode 100644 index 0000000..13bec0c --- /dev/null +++ b/test/packageserver07.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr sync -n temp_remote_dup -d unstable --force +#POST-EXEC +#EXPECT +package server [temp_remote_dup]'s distribution [unstable] has been synchronized. diff --git a/test/packageserver08.testcase b/test/packageserver08.testcase new file mode 100644 index 0000000..db9a935 --- /dev/null +++ b/test/packageserver08.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr gen-snapshot -n temp_remote -s snap01 +#POST-EXEC +#EXPECT +snapshot is generated : diff --git a/test/packageserver09.testcase b/test/packageserver09.testcase new file mode 100644 index 0000000..d8632bf --- /dev/null +++ b/test/packageserver09.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr gen-snapshot -n temp_remote -d unstable -s snap02 +#POST-EXEC +#EXPECT +snapshot is generated : diff --git a/test/packageserver10.testcase b/test/packageserver10.testcase new file mode 100644 index 0000000..7a5fcb2 --- /dev/null +++ b/test/packageserver10.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test +#POST-EXEC +#EXPECT +snapshot is generated : diff --git a/test/packageserver11.testcase b/test/packageserver11.testcase new file mode 100644 index 0000000..247141f --- /dev/null +++ b/test/packageserver11.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr gen-snapshot -n temp_remote -d unstable -s snap03 -b snap01 +#POST-EXEC +#EXPECT +snapshot is generated : diff --git a/test/packageserver12.testcase b/test/packageserver12.testcase new file mode 100644 index 0000000..092eb4e --- /dev/null +++ b/test/packageserver12.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test3 +#POST-EXEC +#EXPECT +snapshot is generated : diff --git a/test/packageserver13.testcase b/test/packageserver13.testcase new file mode 100644 index 0000000..ae8c629 --- /dev/null +++ b/test/packageserver13.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr create -n temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/snap01 +#POST-EXEC +#EXPECT +package server [temp_remote_snap] created successfully diff --git a/test/packageserver14.testcase b/test/packageserver14.testcase new file mode 100644 index 0000000..06bdd06 --- /dev/null +++ b/test/packageserver14.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cp test_server_pkg_file/smart-build-interface* ./ +#EXEC +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen +#POST-EXEC +#EXPECT +package registed successfully diff --git a/test/packageserver15.testcase b/test/packageserver15.testcase new file mode 100644 index 0000000..af34b96 --- /dev/null +++ b/test/packageserver15.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen +#POST-EXEC +#EXPECT +existing package's version is higher than register package diff --git a/test/packageserver16.testcase b/test/packageserver16.testcase new file mode 100644 index 0000000..4d09776 --- /dev/null +++ b/test/packageserver16.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cp test_server_pkg_file/smart-build-interface* ./ +#EXEC +../pkg-svr register -n temp_remote_dup -d unstable -P ./temp_remote/unstable/binary/smart-build-interface_1.20.1_linux.zip --gen --test +#POST-EXEC +#EXPECT +package registed successfully diff --git a/test/packageserver17.testcase b/test/packageserver17.testcase new file mode 100644 index 0000000..ad1549b --- /dev/null +++ b/test/packageserver17.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cp test_server_pkg_file/smart-build-interface* ./ +#EXEC +../pkg-svr remove-pkg -n temp_local -d unstable -P smart-build-interface +#POST-EXEC +#EXPECT +package removed successfully diff --git a/test/packageserver18.testcase b/test/packageserver18.testcase new file mode 100644 index 0000000..431b64d --- /dev/null +++ b/test/packageserver18.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +cp test_server_pkg_file/smart-build-interface* ./ +#EXEC +../pkg-svr list +#POST-EXEC +#EXPECT +temp_remote_snap diff --git a/test/packageserver19.testcase b/test/packageserver19.testcase new file mode 100644 index 0000000..cd5b36b --- /dev/null +++ b/test/packageserver19.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../pkg-svr list -n temp_local +#POST-EXEC +rm smart-build-interface_1.20.1* +#EXPECT +unstable diff --git a/test/packageserver20.testcase b/test/packageserver20.testcase new file mode 100644 index 0000000..7d47e52 --- /dev/null +++ b/test/packageserver20.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../pkg-svr remove -n temp_local --force +#POST-EXEC +YES +#EXPECT +package server [temp_local] removed successfully diff --git a/test/packageserver21.testcase b/test/packageserver21.testcase new file mode 100644 index 0000000..1ae0b53 --- /dev/null +++ b/test/packageserver21.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../pkg-svr remove -n temp_remote --force +#POST-EXEC +YES +#EXPECT +package server [temp_remote] removed successfully diff --git a/test/packageserver22.testcase b/test/packageserver22.testcase new file mode 100644 index 0000000..3dad192 --- /dev/null +++ b/test/packageserver22.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../pkg-svr remove -n temp_remote_dup --force +#POST-EXEC +YES +#EXPECT +package server [temp_remote_dup] removed successfully diff --git a/test/packageserver23.testcase b/test/packageserver23.testcase new file mode 100644 index 0000000..6e37f2f --- /dev/null +++ b/test/packageserver23.testcase @@ -0,0 +1,7 @@ +#PRE-EXEC +#EXEC +../pkg-svr remove -n temp_remote_snap --force +#POST-EXEC +YES +#EXPECT +package server [temp_remote_snap] removed successfully diff --git a/test/packageserver24.testcase b/test/packageserver24.testcase new file mode 100644 index 0000000..2657bab --- /dev/null +++ b/test/packageserver24.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +#EXEC +../pkg-svr add-os -n temp_local -d unstable -o ubuntu-32 +#POST-EXEC +../pkg-svr add-os -n temp_local -d unstable -o windows-32 +#EXPECT +snapshot is generated : +package server add os [ubuntu-32] successfully diff --git a/test/packageserver25.testcase b/test/packageserver25.testcase new file mode 100644 index 0000000..e399ad8 --- /dev/null +++ b/test/packageserver25.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-svr link -n temp_local -d unstable --origin-pkg-name smart-build-interface --origin-pkg-os ubuntu-10.04-32 --link-os-list windows-7-32 +#POST-EXEC +#EXPECT +package linked successfully diff --git a/test/pkg-cli-checkupgrade.testcase b/test/pkg-cli-checkupgrade.testcase new file mode 100644 index 0000000..f9a19be --- /dev/null +++ b/test/pkg-cli-checkupgrade.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli check-upgrade -l pkgcli01 -u http://172.21.111.132/testserver2/unstable +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +"base-ide-product" package : 0.20.8 -> +Info: Checked packages for upgrading.. OK diff --git a/test/pkg-cli-clean-f.testcase b/test/pkg-cli-clean-f.testcase new file mode 100644 index 0000000..fd4543d --- /dev/null +++ b/test/pkg-cli-clean-f.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p nativeapp-eplugin -u http://172.21.111.132/testserver3/unstable -t -l pkgcli01 +../pkg-cli clean -l pkgcli01 -f +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +Info: There is no any package. diff --git a/test/pkg-cli-download-t.testcase b/test/pkg-cli-download-t.testcase new file mode 100644 index 0000000..6d7263b --- /dev/null +++ b/test/pkg-cli-download-t.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli download -p nativeapp-eplugin -u http://172.21.111.132/testserver3/unstable -t -l pkgcli01 +ls pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product_0.20.8_linux.zip +common-eplugin_0.20.6_linux.zip +nativeapp-eplugin_0.20.4_linux.zip +nativecommon-eplugin_0.20.1_linux.zip diff --git a/test/pkg-cli-download.testcase b/test/pkg-cli-download.testcase new file mode 100644 index 0000000..f4e41d1 --- /dev/null +++ b/test/pkg-cli-download.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +cd pkgcli01; ../../pkg-cli download -p base-ide-product -u http://172.21.111.132/testserver3/unstable +ls pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product_1.0.2_linux.zip diff --git a/test/pkg-cli-install-f.testcase b/test/pkg-cli-install-f.testcase new file mode 100644 index 0000000..7794646 --- /dev/null +++ b/test/pkg-cli-install-f.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver2/unstable -l pkgcli01 +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 -f +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) diff --git a/test/pkg-cli-install-t.testcase b/test/pkg-cli-install-t.testcase new file mode 100644 index 0000000..00d2734 --- /dev/null +++ b/test/pkg-cli-install-t.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p nativeapp-eplugin -u http://172.21.111.132/testserver3/unstable -t -l pkgcli01 +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) +common-eplugin (0.20.6) +nativeapp-eplugin (0.20.4) +nativecommon-eplugin (0.20.1) diff --git a/test/pkg-cli-install.testcase b/test/pkg-cli-install.testcase new file mode 100644 index 0000000..31ac836 --- /dev/null +++ b/test/pkg-cli-install.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) diff --git a/test/pkg-cli-installfile-f.testcase b/test/pkg-cli-installfile-f.testcase new file mode 100644 index 0000000..810ad2f --- /dev/null +++ b/test/pkg-cli-installfile-f.testcase @@ -0,0 +1,13 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver2/unstable -l pkgcli01 +../pkg-cli download -p base-ide-product -u http://172.21.111.132/testserver3/unstable +../pkg-cli install-file -p base-ide-product_0.20.8_linux.zip -l pkgcli01 -f +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -f base-ide-product_0.20.8_linux.zip +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) diff --git a/test/pkg-cli-installfile.testcase b/test/pkg-cli-installfile.testcase new file mode 100644 index 0000000..ebdb5de --- /dev/null +++ b/test/pkg-cli-installfile.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli download -p base-ide-product -u http://172.21.111.132/testserver3/unstable +../pkg-cli install-file -p base-ide-product_0.20.8_linux.zip -l pkgcli01 +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -f base-ide-product_0.20.8_linux.zip +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) diff --git a/test/pkg-cli-listlpkg.testcase b/test/pkg-cli-listlpkg.testcase new file mode 100644 index 0000000..31ac836 --- /dev/null +++ b/test/pkg-cli-listlpkg.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.8) diff --git a/test/pkg-cli-listrpkg.testcase b/test/pkg-cli-listrpkg.testcase new file mode 100644 index 0000000..84bf751 --- /dev/null +++ b/test/pkg-cli-listrpkg.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-cli list-rpkg -u http://172.21.111.132/testserver3/unstable +#POST-EXEC +#EXPECT +base-ide-product (1.0.2) diff --git a/test/pkg-cli-showlpkg.testcase b/test/pkg-cli-showlpkg.testcase new file mode 100644 index 0000000..dbee48c --- /dev/null +++ b/test/pkg-cli-showlpkg.testcase @@ -0,0 +1,12 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli show-lpkg -p base-ide-product -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +Package : base-ide-product +Version : 0.20.8 +OS : linux diff --git a/test/pkg-cli-showrpkg.testcase b/test/pkg-cli-showrpkg.testcase new file mode 100644 index 0000000..fc5cf62 --- /dev/null +++ b/test/pkg-cli-showrpkg.testcase @@ -0,0 +1,8 @@ +#PRE-EXEC +#EXEC +../pkg-cli show-rpkg -p base-ide-product -u http://172.21.111.132/testserver3/unstable +#POST-EXEC +#EXPECT +Package : base-ide-product +Version : 1.0.2 +OS : linux diff --git a/test/pkg-cli-source.testcase b/test/pkg-cli-source.testcase new file mode 100644 index 0000000..64053ad --- /dev/null +++ b/test/pkg-cli-source.testcase @@ -0,0 +1,10 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +cd pkgcli01; ../../pkg-cli source -p base-ide-product -u http://172.21.111.132/testserver3/unstable +ls pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +product_0.20.8.tar.gz diff --git a/test/pkg-cli-uninstall-t.testcase b/test/pkg-cli-uninstall-t.testcase new file mode 100644 index 0000000..749815f --- /dev/null +++ b/test/pkg-cli-uninstall-t.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p nativeapp-eplugin -u http://172.21.111.132/testserver3/unstable -l pkgcli01 -t +../pkg-cli uninstall -p base-ide-product -l pkgcli01 -t +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +Info: There is no any package. diff --git a/test/pkg-cli-uninstall.testcase b/test/pkg-cli-uninstall.testcase new file mode 100644 index 0000000..10e7647 --- /dev/null +++ b/test/pkg-cli-uninstall.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product - http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli uninstall -p base-ide-product -l pkgcli01 +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +Info: There is no any package. diff --git a/test/pkg-cli-update.testcase b/test/pkg-cli-update.testcase new file mode 100644 index 0000000..3c6fa52 --- /dev/null +++ b/test/pkg-cli-update.testcase @@ -0,0 +1,6 @@ +#PRE-EXEC +#EXEC +../pkg-cli update -u http://172.21.111.132/testserver3/unstable +#POST-EXEC +#EXPECT +Update package list from "http://172.21.111.132/testserver3/unstable".. OK diff --git a/test/pkg-cli-upgrade.testcase b/test/pkg-cli-upgrade.testcase new file mode 100644 index 0000000..493d947 --- /dev/null +++ b/test/pkg-cli-upgrade.testcase @@ -0,0 +1,11 @@ +#PRE-EXEC +rm -rf pkgcli01 +mkdir pkgcli01 +#EXEC +../pkg-cli install -p base-ide-product -u http://172.21.111.132/testserver3/unstable -l pkgcli01 +../pkg-cli upgrade -l pkgcli01 -u http://172.21.111.132/testserver2/unstable +../pkg-cli list-lpkg -l pkgcli01 +#POST-EXEC +rm -rf pkgcli01 +#EXPECT +base-ide-product (0.20.14) diff --git a/test/pkg-cli.testsuite b/test/pkg-cli.testsuite new file mode 100644 index 0000000..21c2fe0 --- /dev/null +++ b/test/pkg-cli.testsuite @@ -0,0 +1,18 @@ +pkg-cli-update.testcase +pkg-cli-listrpkg.testcase +pkg-cli-showrpkg.testcase +pkg-cli-download.testcase +pkg-cli-download-t.testcase +pkg-cli-source.testcase +pkg-cli-install.testcase +pkg-cli-install-t.testcase +pkg-cli-install-f.testcase +pkg-cli-uninstall.testcase +pkg-cli-uninstall-t.testcase +pkg-cli-installfile.testcase +pkg-cli-installfile-f.testcase +pkg-cli-listlpkg.testcase +pkg-cli-showlpkg.testcase +pkg-cli-checkupgrade.testcase +pkg-cli-upgrade.testcase +pkg-cli-clean-f.testcase diff --git a/test/pkg-list b/test/pkg-list index cf64628..55d5272 100644 --- a/test/pkg-list +++ b/test/pkg-list @@ -1,24 +1,40 @@ -Package : A +Source : Origin Version : 0.1.0 +Maintainer : taejun.ha + +Package : A OS : linux +C-test : test Build-host-os :linux | windows | darwin -Maintainer : taejun.ha Path : binary/A_0.1.0_linux.zip +C-commic : ask Origin : remote -SHA256 : 52b400554f2a29dec46144af649181cf287c000b4feb65de72055ed9f11924a9 +C-origin : kkk +Description : this is my first +script +#Changes : kkk +#sa;ldfkj +#alsdkfj +#lsdkfj Package: B -Version : 0.2.0 OS : linux Build-host-os :linux | windows | darwin -Maintainer : taejun.ha Install-dependency : C, D, E Build-dependency : F (>= 1.0.0.20101221), E (>= 1.0.0.20101221) Source-dependency : D, scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) Path : -Source : Origin -From-server? : true SHA256 : your_checksum Description : this is my first -project -descriotion +script +C-kim : oks +Change-log : +* 0.1.0 +test + ttkk +* 1.0.8 +Add change log function + pkginfo.manifest include "change log" section +* 1.0.7 +dibs document change + you can read dibs documents in ubuntu using PDF readeranges diff --git a/test/pkg-list-local b/test/pkg-list-local new file mode 100644 index 0000000..ce331b6 --- /dev/null +++ b/test/pkg-list-local @@ -0,0 +1,17 @@ +Include : pkg-list + +Package : A +OS : windows +Build-host-os :linux | windows | darwin +Path : binary/A_0.1.0_linux.zip +Origin : remote + +Package: B +OS : windows +Build-host-os :linux | windows | darwin +Install-dependency : C, D, E +Build-dependency : F (>= 1.0.0.20101221), E (>= 1.0.0.20101221) +Source-dependency : D, scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) +Path : +SHA256 : your_checksum +Description : this is my first diff --git a/test/pkginfo.manifest b/test/pkginfo.manifest index ebace22..da69b2d 100644 --- a/test/pkginfo.manifest +++ b/test/pkginfo.manifest @@ -5,14 +5,17 @@ Build-host-os:linux | windows | darwin Maintainer : your_name name Install-dependency : codecoverage, rootstrap-slp-device-1.0, slp-ide Build-dependency : scratchbox-aquila-device-rootstrap (>= 1.0.0.20101221), scratchbox-aquila-simulator-rootstrap (>= 1.0.0.20101221), scratchbox-core (>= 1.0.17) -Build-src-dependency : scratchbox-aquila-device-rootstrap (>= 1.0.0.20101221) [linux|windows], scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) +#Build-src-dependency : scratchbox-aquila-device-rootstrap (>= 1.0.0.20101221) [linux|windows], scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) Description : this is my first +project that +is my +preciuse project : descriotion : Attribute : -Install-script : -Remove-script : -Category : +#Install-script : +#Remove-script : +#Category : Conflicts : Source : origin @@ -23,13 +26,15 @@ Build-host-os:linux | windows | darwin Maintainer : your_name name Install-dependency : codecoverage, rootstrap-slp-device-1.0, slp-ide Build-dependency : scratchbox-aquila-device-rootstrap (>= 1.0.0.20101221), scratchbox-aquila-simulator-rootstrap (>= 1.0.0.20101221), scratchbox-core (>= 1.0.17) -Build-src-dependency : scratchbox-aquila-device-rootstrap (>= 1.0.0.20101221) [linux|windows], scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) +Source-dependency : scratchbox-aquila-simulator-rootstrap [ linux |windows ](>= 1.0.0.20101221), scratchbox-core [windows|darwin](>= 1.0.17) Description : this is my first +hello +project project : descriotion : Attribute : -Install-script : -Remove-script : -Category : +C-ategory : +#Install-script : +#Remove-script : Conflicts : Source : origin diff --git a/test/pkgsvr2.init b/test/pkgsvr2.init new file mode 100755 index 0000000..e97681c --- /dev/null +++ b/test/pkgsvr2.init @@ -0,0 +1,7 @@ +#!/bin/sh +rm -rf ~/.build_tools/pkg_server/pkgsvr02 +rm -rf `pwd`/pkgsvr02 +ruby -d ../pkg-svr create -n pkgsvr02 -d unstable +ruby -d ../pkg-svr add-os -n pkgsvr02 -d unstable -o ubuntu-32 +ruby -d ../pkg-svr add-os -n pkgsvr02 -d unstable -o windows-32 +ruby -d ../pkg-svr start -n pkgsvr02 -p 4444 diff --git a/test/regression.rb b/test/regression.rb new file mode 100755 index 0000000..11aa893 --- /dev/null +++ b/test/regression.rb @@ -0,0 +1,203 @@ +#!/usr/bin/ruby +$success_cases=[] +$failure_cases=[] +$testcases=[] + +$total_cnt = 0 + +testCaseName = "" +resultCheck = "" +resultFlag = "" +resultCmdCount = 0 + + +class TestCase + attr_accessor :name, :pre_exec_cmds, :exec_cmds, :post_exec_cmds, :expected_results + def initialize(name) + @name = name + @pre_exec_cmds = [] + @exec_cmds = [] + @post_exec_cmds = [] + @expected_results = [] + end + + def is_succeeded?(results) + i = 0 + @expected_results.each do |e| + found = false + if results[i].nil? then + return false + end + if not results[i].include? e then + return false + end + i += 1 + end + + return true + end +end + + +# parse +def parse_testcase(file_name) + # create + tcase = TestCase.new( file_name ) + + # parse + File.open(file_name,"r") do |f| + status="START" + f.each_line do |l| + ln = l.strip + if ln == "#PRE-EXEC" or ln == "#EXEC" or + ln == "#POST-EXEC" or ln == "#EXPECT" then + + status = ln + else + case status + when "#PRE-EXEC" + tcase.pre_exec_cmds.push ln + when "#EXEC" + tcase.exec_cmds.push ln + when "#POST-EXEC" + tcase.post_exec_cmds.push ln + when "#EXPECT" + tcase.expected_results.push ln + else + # ignore + end + end + end + end + + return tcase +end + + +# test execution +def execute( file_name ) + printf("#{file_name} ... ") + STDOUT.flush + + # parse + tcase = parse_testcase( file_name ) + + # pre-exec + tcase.pre_exec_cmds.each do |cmd| + fork_p = false + if cmd[-1,1] == "&" then + cmd = cmd[0..-2] + fork_p = true + end + + # get result + if not fork_p then + IO.popen("#{cmd} 2>&1") { |io| + # io.each do |line| + # puts "---> #{line}" + # end + } + else + IO.popen("#{cmd} 2>&1 &") { |io| + # io.each do |line| + # puts "---> #{line}" + # end + } + end + #`#{cmd}` + end + + # exec + results = [] + tcase.exec_cmds.each do |cmd| + fork_p = false + hidden_p = false + if cmd[-1,1] == "&" then + cmd = cmd[0..-2] + fork_p = true + end + if cmd[0,1] == ">" then + cmd = cmd[1..-1] + hidden_p = true + end + # get result + if fork_p then + IO.popen("#{cmd} 2>&1 &") { |io| + io.each do |line| + if not hidden_p then + results.push line.strip + #puts "---> #{line}" + end + end + } + else + IO.popen("#{cmd} 2>&1") { |io| + io.each do |line| + if not hidden_p then + results.push line.strip + #puts "---> #{line}" + end + end + } + end + end + + # check expected result + if tcase.is_succeeded?(results) then + puts "SUCCESS" + $success_cases.push file_name + else + puts "FAIL" + $failure_cases.push file_name + results.each do |l| + puts ">" + l + end + end + + # post-exec + tcase.post_exec_cmds.each do |cmd| + # get result + IO.popen("#{cmd} 2>&1") { |io| + } + #`#{cmd}` + end +end + + +#test_list file open +if (ARGV.size() == 0) + testsuite_name = "list.txt" +else + testsuite_name = ARGV.shift +end + +# execute testsuite +puts +puts "Regression Test Start " +puts "==================================" + +File.open( testsuite_name ) do |f| + f.each_line do |line| + $testcases.push("#{line.strip}") + execute(line.strip) + end +end + +# print result +puts +puts "Regression Test Result" +puts "----------------------" +puts "Total Test Case : #{$testcases.count}" +puts "Test Success : #{$success_cases.count}" +puts "Test Errors : #{$failure_cases.count}" +puts + +if $failure_cases.count != 0 then + puts "Test Fail Files" + puts "---------------" + $failure_cases.each do |name| + puts name + end + puts +end + diff --git a/test/test.sh b/test/test.sh new file mode 100755 index 0000000..e765f43 --- /dev/null +++ b/test/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh +./regression.rb pkg-cli.testsuite +./regression.rb packageserver.testsuite +./regression.rb buildserver.testsuite +./regression.rb buildcli.testsuite diff --git a/test/test_pkglist_parser.rb b/test/test_pkglist_parser.rb index c299f7f..0c11ace 100755 --- a/test/test_pkglist_parser.rb +++ b/test/test_pkglist_parser.rb @@ -2,8 +2,9 @@ require '../src/common/parser' require '../src/common/package' -alist = Parser.read_pkg_list "pkg-list" -a_list = alist.values -a_list.each do |l| +alist = Parser.read_multy_pkginfo_from "pkg-list" +alist.each do |l| l.print -end + puts l.change_log_string + puts "" +end diff --git a/test/test_server b/test/test_server index 64bdb77..c3f18a0 100755 --- a/test/test_server +++ b/test/test_server @@ -1,65 +1,66 @@ #!/bin/sh echo "============ remove 1 ==============" -../pkg-svr remove -i temp_local +../pkg-svr remove -n temp_local --force echo "============ remove 2 ==============" -../pkg-svr remove -i temp_remote +../pkg-svr remove -n temp_remote --force echo "============ remove 3 ==============" -../pkg-svr remove -i temp_remote_dup +../pkg-svr remove -n temp_remote_dup --force echo "============ remove 4 ==============" -../pkg-svr remove -i temp_remote_snap +../pkg-svr remove -n temp_remote_snap --force echo "============ create 1 ==============" -../pkg-svr create -i temp_local -d unstable +../pkg-svr create -n temp_local -d unstable echo "============ create 2 ==============" -../pkg-svr create -i temp_remote -d unstable -u http://172.21.17.55/pkgserver/unstable +../pkg-svr create -n temp_remote -d unstable -u http://172.21.17.55/private/develop echo "============ create 3 ==============" -../pkg-svr create -i temp_remote_dup -d unstable -u temp_remote/unstable +../pkg-svr create -n temp_remote_dup -d unstable -u temp_local/unstable echo "============ add dist 1 ==============" -../pkg-svr add-dist -i temp_local -d stable +../pkg-svr add-dist -n temp_local -d stable echo "============ sync 1 ==============" -../pkg-svr sync -i temp_remote -d unstable +../pkg-svr sync -n temp_remote -d unstable echo "============ sync 2 ==============" -../pkg-svr sync -i temp_remote_dup -d unstable -f +../pkg-svr sync -n temp_remote_dup -d unstable --force echo "============ gen snapshot 1 ==============" -../pkg-svr gen-snapshot -i temp_remote +../pkg-svr gen-snapshot -n temp_remote echo "============ gen snapshot 2 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable +../pkg-svr gen-snapshot -n temp_remote -d unstable echo "============ gen snapshot 3 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test echo "============ gen snapshot 4 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -n test2 -b test +../pkg-svr gen-snapshot -n temp_remote -d unstable -s test2 -b test echo "============ gen snapshot 5 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o linux +../pkg-svr gen-snapshot -n temp_remote -d unstable -o linux echo "============ gen snapshot 6 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o windows +../pkg-svr gen-snapshot -n temp_remote -d unstable -o windows echo "============ gen snapshot 7 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o darwin +../pkg-svr gen-snapshot -n temp_remote -d unstable -o darwin echo "============ gen snapshot 8 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o all +../pkg-svr gen-snapshot -n temp_remote -d unstable -o all echo "============ gen snapshot 9 ==============" -../pkg-svr gen-snapshot -i temp_remote -d unstable -o all -p test_server_pkg_file/smart-build-interface_1.20.1_linux.zip -n test3 +../pkg-svr gen-snapshot -n temp_remote -d unstable -o all -P test_server_pkg_file/smart-build-interface_1.20.1_linux.zip -s test3 echo "============ create 4 ==============" -../pkg-svr create -i temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/test +../pkg-svr create -n temp_remote_snap -d unstable -u temp_remote/unstable/snapshots/test echo "============ register 1 ==============" -cp test_server_pkg_file/smart-build-interface_1.20.1_linux.zip ./smart-build-interface_1.20.1_linux.zip -cp test_server_pkg_file/smart-build-interface_1.20.1.tar.gz ./smart-build-interface_1.20.1.tar.gz -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g -echo "============ spkg path 1 ==============" -../pkg-svr spkg-path -i temp_remote -d unstable -s smart-build-interface_1.20.1.tar.gz +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_local -d unstable -P smart-build-interface_1.20.1_linux.zip echo "============ register 2 ==============" -cp test_server_pkg_file/smart-build-interface_1.20.1_linux.zip ./smart-build-interface_1.20.1_linux.zip -cp test_server_pkg_file/smart-build-interface_1.20.1.tar.gz ./smart-build-interface_1.20.1.tar.gz -../pkg-svr register -i temp_remote -d unstable -p smart-build-interface_1.20.1_linux.zip -s smart-build-interface_1.20.1.tar.gz -g +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_remote -d unstable -P smart-build-interface_1.20.1_linux.zip --gen echo "============ register 3 ==============" -../pkg-svr register -i temp_remote_dup -d unstable -p ./temp_remote/unstable/binary/smart-build-interface_1.20.1_linux.zip -s ./temp_remote/unstable/source/smart-build-interface_1.20.1.tar.gz -g -t +cp test_server_pkg_file/smart-build-interface_*_linux.zip ./ +../pkg-svr register -n temp_remote_dup -d unstable -P smart-build-interface_1.20.1_linux.zip --gen --test +echo "============ register 4 ==============" +cp test_server_pkg_file/archive.zip ./ +../pkg-svr register -n temp_local -d unstable -A archive.zip echo "============ remove 3 ==============" -../pkg-svr remove-pkg -i temp_remote_dup -d unstable -p smart-build-interface +../pkg-svr remove-pkg -n temp_local -d unstable -P smart-build-interface +echo "============ clean 1 ==============" +../pkg-svr clean -n temp_local -d unstable +echo "============ clean 2 ==============" +../pkg-svr clean -n temp_remote -d unstable -s test,test2,test3 echo "============ list 1 ==============" ../pkg-svr list echo "============ list 2 ==============" -../pkg-svr list -i temp_local -#../pkg-svr remove -i temp +../pkg-svr list -n temp_local +#../pkg-svr remove -n temp -#cleanup -rm smart-build-interface_1.20.1_linux.zip -rm smart-build-interface_1.20.1.tar.gz diff --git a/test/test_server_pkg_file/archive.zip b/test/test_server_pkg_file/archive.zip new file mode 100644 index 0000000000000000000000000000000000000000..140bd05bd7f6f55d6ea86f41bc75c8257b3b8150 GIT binary patch literal 308 zcmWIWW@Zs#U|`^2U~gOFFm;+sLmiN}0f+?{WEcvv(=+qZ^7V2P^D@&?i%UX7I2oAt zimvkq;nE6j21b?_%nS@*qO|uQ7n326%lp4we-2E$I$LSpH?>6pALX*I^{!jFM7JY8 z{|0Nm9GjYP{~5u@%=s+YC;YEmiqY!7-nMkYH;I#TJ&n~j)?NLT`da**z#ECkSGV6@ zbP!(Y%K9>nmF>=%bNo9>6+?Uf?=PRS_|xh$+``Pl%=T=*mJ~eV=WNM)zAwO=kx7mj pmro>s{$XHX1mY!)AQq;-SRwvG^HYE~D;r2XBM^oG=@TFh0{|WrWfA}Y literal 0 HcmV?d00001 diff --git a/test/test_server_pkg_file/smart-build-interface_1.20.1_linux.zip b/test/test_server_pkg_file/smart-build-interface_1.20.1_linux.zip index 109a1441f2e8f0886379fe411d93b59602e30e58..cc2a217be42c4fc3ef3b9b362f42f5521eac6002 100644 GIT binary patch literal 308 zcmWIWW@Zs#U|`^2*faZ;!_;Xm4Rt`?1|Sw-kYOmuPS4Cs%h$_I%*#wmEiMTS;bdS| zX8Gd}!lf1542&!C$rL}~9qE+#`Bm-m0W{v4Qgb+*#HZ)%GIKFVcZ>s_~UiEc-H z{tecAIW{%p{xgD)ne$n)PxxQC6r01)ZxScxdK#;5th@Rv^|kmrfj1J7uWrA+ z=pek(mGxyDE8Cqj=lFM&Du(v{-(Nmu@u$^ixP_U8neEwrEh%`!&)Jgqd|!Y!Ba<96 pE}uvM{lmb(2*gVoK`cyvu|oWX=BEH}RyL4&Mj#9W(kDP11_0@HW(xoS literal 327 zcmWIWW@Zs#U|`^2U{&*SFz$9po(SY!0Ac|K8HR%F^vt}pe7)Snyv(%J;*!u1P6p;* zN+$jwTw1}+z{v7~nSlXJl=dFvI%FW=^8RPn-^OLv7I%nO3$GOTxUcuN*)6qAo(Fz? z)zhu`Y+zvhd&bTMwMqUPnk^KUFFKpINBo@nBR<`g(uE}lPw&5UyLVkk@{#^?&$nAG zE>KG96uP8bEm5%F%JzZd%n3LD>95y}DqFvWPlrc`=a0k-ro&fjPc*Oef7Q|X>%is* zMbokq=Oiy$nPWKV7gK;YBa<96F3(8-y~x182*gVoK`czKvO>Iy=FI?aRyL4&Mj#9W J(zihz1^^f9Z_NMz diff --git a/test/upgrade/01.testcase b/test/upgrade/01.testcase new file mode 100644 index 0000000..af117a1 --- /dev/null +++ b/test/upgrade/01.testcase @@ -0,0 +1,9 @@ +#PRE-EXEC +./dibs1/build-svr upgrade -n testserver3 -D unstable +#EXEC +sleep 3 +ls dibs1/VERSION dibs2/VERSION +#POST-EXEC +#EXPECT +dibs1/VERSION +dibs2/VERSION diff --git a/test/upgrade/buildsvr1.init b/test/upgrade/buildsvr1.init new file mode 100755 index 0000000..0578b61 --- /dev/null +++ b/test/upgrade/buildsvr1.init @@ -0,0 +1,34 @@ +#!/bin/sh +CURDIR=`pwd` +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby " +fi + +rm -rf buildsvr01 +rm -rf git01 +rm -rf bin +rm -rf ~/.build_tools/build_server/testserver3 +rm -rf dibs1 +mkdir dibs1 +cp -r ../../src dibs1/ +cp -r ../../build-svr dibs1/ +cp -r ../../pkg-svr dibs1/ +cp -r ../../upgrade dibs1/ +cd ../../ +./pkg-build -u $CURDIR/pkgsvr01/unstable +cd $CURDIR + +dibs1/build-svr remove -n testserver3 +mkdir buildsvr01 +cd buildsvr01 +${RUBY} ../dibs1/build-svr create -n testserver3 +${RUBY} ../dibs1/build-svr migrate -n testserver3 +cd .. + +${RUBY} dibs1/build-svr add-dist -n testserver3 -D unstable -u $CURDIR/pkgsvr01/unstable -d 127.0.0.1:3333 +${RUBY} dibs1/build-svr add-svr -n testserver3 -d 127.0.0.1:2224 +${RUBY} dibs1/build-svr add-os -n testserver3 -o ubuntu-32 +${RUBY} dibs1/pkg-svr register -n pkgsvr01 -d unstable -P ../../dibs_*.zip + +${RUBY} dibs1/build-svr start -n testserver3 -p 2223 + diff --git a/test/upgrade/buildsvr2.init b/test/upgrade/buildsvr2.init new file mode 100755 index 0000000..ac5ccf7 --- /dev/null +++ b/test/upgrade/buildsvr2.init @@ -0,0 +1,22 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby " +fi + +rm -rf buildsvr02 +rm -rf dibs2 +mkdir dibs2 +cp -r ../../src dibs2/ +cp -r ../../build-svr dibs2/ +cp -r ../../pkg-svr dibs2/ +cp -r ../../upgrade dibs2/ + +../../build-svr remove -n testserver4 +mkdir buildsvr02 +cd buildsvr02 +${RUBY} ../dibs2/build-svr create -n testserver4 +${RUBY} ../dibs2/build-svr migrate -n testserver4 +cd .. +${RUBY} dibs2/build-svr add-dist -n testserver4 -D unstable -u `pwd`/pkgsvr01/unstable -d 127.0.0.1:3333 + +${RUBY} dibs2/build-svr start -n testserver4 -p 2224 diff --git a/test/upgrade/pkgsvr.init b/test/upgrade/pkgsvr.init new file mode 100755 index 0000000..d0c7cd9 --- /dev/null +++ b/test/upgrade/pkgsvr.init @@ -0,0 +1,9 @@ +#!/bin/sh +if [ "x${RUBY}" = "x" ] ;then + RUBY="ruby -d" +fi +rm -rf ~/.build_tools/pkg_server/pkgsvr01 +rm -rf `pwd`/pkgsvr01 +${RUBY} ../../pkg-svr create -n pkgsvr01 -d unstable +${RUBY} ../../pkg-svr add-os -n pkgsvr01 -d unstable -o ubuntu-32 +${RUBY} ../../pkg-svr start -n pkgsvr01 -p 3333 diff --git a/test/upgrade/testsuite b/test/upgrade/testsuite new file mode 100644 index 0000000..1bb2eb2 --- /dev/null +++ b/test/upgrade/testsuite @@ -0,0 +1 @@ +01.testcase diff --git a/tizen-ide/get_ide_sources.sh b/tizen-ide/get_ide_sources.sh new file mode 100755 index 0000000..b7c58d9 --- /dev/null +++ b/tizen-ide/get_ide_sources.sh @@ -0,0 +1,276 @@ +#!/bin/bash + +############################################################### +## Variables +############################################################### + +START_PATH=~+ ## like `pwd` +SCRIPT_NAME=$0 +SCRIPT_OPERATION=$1 +ARG1=$2 +ARG2=$3 +ARG3=$4 +GIT_PORT=29419 +CONTINUE=n +GIT_LIST=" +/sdk/ide/common-eplugin +/sdk/ide/eventinjector-eplugin +/sdk/ide/nativecommon-eplugin +/sdk/ide/nativeappcommon-eplugin +/sdk/ide/native-eplugin +/sdk/ide/native-ext-eplugin +/sdk/ide/native-sample +/sdk/ide/nativeplatform-eplugin +/sdk/ide/unittest-eplugin +/sdk/ide/profiler-eplugin +/sdk/ide/codecoverage-eplugin +/sdk/ide/assignmenttracing-eplugin +/sdk/ide/webapp-eplugin +/sdk/ide/product +/sdk/ide/websimulator-eplugin +/sdk/tools/sdb +" +# /sdk/ide/nativeapp-eplugin +# /sdk/ide/native-gui-builder-eplugin +# /sdk/gui-builder/native-gui-builder +# /sdk/ide/telephony-eplugin +# /sdk/ide/codehiding-eplugin + +############################################################### +## Usage output functions +############################################################### + +function usage() { + echo "Usage : ${SCRIPT_NAME##*/} []"; echo + echo "The most commonly used script commands are :" + echo " clone Clone git sources about Tizen SDK" + echo " pull Pull git sources about Tizen SDK" + echo " checkout checkout git sources about Tizen SDK"; echo + exit 1 +} + +function usage_pull() { + echo "Usage : ${SCRIPT_NAME##*/} pull : Git pull in current directory"; + echo "Usage : ${SCRIPT_NAME##*/} pull : Git pull in source directory"; + + draw_line + echo " Ex1) \$ ${SCRIPT_NAME} pull"; echo + echo " Ex2) \$ ${SCRIPT_NAME} pull $(pwd)/tizen-ide-sources"; echo + exit 1 +} + +function usage_checkout() { + echo "Usage : ${SCRIPT_NAME##*/} checkout : Git checkout in current directory"; + echo "Usage : ${SCRIPT_NAME##*/} checkout : Git checkout in source directory"; + + draw_line + echo " Ex1) \$ ${SCRIPT_NAME} checkout develop"; echo + echo " Ex2) \$ ${SCRIPT_NAME} checkout develop $(pwd)/tizen-ide-sources"; echo + exit 1 +} + +function usage_clone() { + draw_line + echo "Usage : 1) ${SCRIPT_NAME##*/} clone : Git clone in curreut directory"; echo + echo " 2) ${SCRIPT_NAME##*/} clone : Git clone in destination directory" + draw_line + echo " Ex1) \$ ${SCRIPT_NAME} clone develop gerrithost" + echo " Ex2) \$ ${SCRIPT_NAME} clone release http://develop.tizen.org/git:2039 /home/usr/work/git" + exit 1 +} + + +############################################################### +## Processing Functions +############################################################### + +function draw_line() { + echo; echo "==========================================================================="; echo; +} + +## Error Check Function +function isError() { + ERROR_CODE=$? + + if [ ${ERROR_CODE} == 0 ]; then + echo "[ $1 : Done ]"; + else + echo "[ $1 : Fail (ErrorCode : ${ERROR_CODE}) ]" + if [ ${CONTINUE} == "n" ]; then + input=0 + while [ ${input} != "y" -a ${input} != "n" -a ${input} != "a" ]; do + echo "Continue? y: Yes, n: No, a: Yes to all" + read input + if [ ${input} == "n" ]; then + exit ${ERROR_CODE} + elif [ ${input} == "a" ]; then + CONTINUE=y + echo ${CONTINUE} + fi + done + fi + fi +} + +## Cloning git +function git_clone() { + GIT_PATH=$1 + GIT_NAME=${GIT_PATH##*/} + + ## ARG1 : + ## ARG2 : + ## ARG3 : + git clone -b ${ARG1} ${ARG2}:${GIT_PATH} ${ARG3}/${GIT_NAME} + isError "Cloned ${GIT_NAME}" + scp -p -P ${GIT_PORT} ${ARG2}:hooks/commit-msg ${ARG3}/${GIT_NAME}/.git/hooks/ + isError "Generate change-id ${GIT_NAME}" +} + +## Cloning git all +function git_clone_all() { + draw_line; echo "Git clone sources"; draw_line + + for GIT_EACH in ${GIT_LIST} + do + git_clone ${GIT_EACH} + done +} + +## Pulling git +function git_pull() { + GIT_PATH=$1 + GIT_NAME=${GIT_PATH##*/} + + ## ARG1 : + cd ${ARG1}/${GIT_NAME} + isError "Found git directory ( ${ARG1}/${GIT_NAME} )" + git pull + isError "Pulled ${GIT_NAME}" +} + +## Pulling git all +function git_pull_all() { + draw_line; echo "Git pull sources"; draw_line + + cd ${ARG1} + isError "Checked source directory ( ${ARG1} )" + + for GIT_EACH in ${GIT_LIST} + do + git_pull ${GIT_EACH} + done + + cd ${START_PATH} +} + +## Checking out git +function git_checkout() { + GIT_PATH=$1 + GIT_NAME=${GIT_PATH##*/} + + ## ARG1 : + cd ${ARG2}/${GIT_NAME} + isError "Found git directory ( ${ARG2}/${GIT_NAME} )" + git checkout ${ARG1} + isError "Checkout ${GIT_NAME}" +} + +## Checking out git all +function git_checkout_all() { + draw_line; echo "Git checkout"; draw_line + + cd ${ARG2} + isError "Checked source directory ( ${ARG1} )" + + for GIT_EACH in ${GIT_LIST} + do + git_checkout ${GIT_EACH} + done + + cd ${START_PATH} +} + +## Command git +function git_command() { + GIT_PATH=$1 + GIT_NAME=${GIT_PATH##*/} + + ## ARG1 : + cd ${ARG1}/${GIT_NAME} + isError "Found git directory ( ${ARG1}/${GIT_NAME} )" + git ${SCRIPT_OPERATION} + isError "Pulled ${GIT_NAME}" +} + +## Command git all +function git_command_all() { + draw_line; echo "Git ${SCRIPT_OPERATION}"; draw_line + + cd ${ARG1} + isError "Checked source directory ( ${ARG1} )" + + for GIT_EACH in ${GIT_LIST} + do + git_command ${GIT_EACH} + done + + cd ${START_PATH} +} + +############################################################### +## Begin script +############################################################### + +case ${SCRIPT_OPERATION} in + ## process "clone" operation + clone) + if [ "$#" == 4 ]; then + git_clone_all + elif [ "$#" == 3 ]; then + ARG3=$(pwd) + git_clone_all + else + usage_clone + fi + ;; + + ## process "pull" operation + pull) + if [ "$#" == 2 ]; then + git_pull_all + elif [ "$#" == 1 ]; then + ARG1=$(pwd) + git_pull_all + else + usage_pull + fi + ;; + + ## process "checkout" operation + checkout) + if [ "$#" == 3 ]; then + git_checkout_all + elif [ "$#" == 2 ]; then + ARG2=$(pwd) + git_checkout_all + else + usage_checkout + fi + ;; + + ## process default + *) + if [ "$#" == 1 ]; then + ARG1=$(pwd) + git_command_all + else + usage + fi + ;; +esac + +echo "[ Finished process ]" + +############################################################### +## End script +############################################################### diff --git a/upgrade b/upgrade new file mode 100755 index 0000000..26120ad --- /dev/null +++ b/upgrade @@ -0,0 +1,285 @@ +#!/usr/bin/ruby +=begin + + upgrade + +Copyright (c) 2000 - 2011 Samsung Electronics Co., Ltd. All rights reserved. + +Contact: +Taejun Ha +Jiil Hyoun +Donghyuk Yang +DongHee Yang +Sungmin Kim + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Contributors: +- S-Core Co., Ltd +=end + +require 'fileutils' +require 'optparse' +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/common" +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/build_server" +$LOAD_PATH.unshift File.dirname(__FILE__)+"/src/pkg_server" +require "BuildServerController" +require "utils.rb" +require "log.rb" +require "client" + +def option_error_check( options ) + if options[:start] then + if options[:locate].nil? or options[:type].nil? or options[:name].nil? then + raise ArgumentError, "upgrade -l -S -t -n -p -D " + "\n" + end + else + if options[:locate].nil? or options[:url].nil? then + raise ArgumentError, "upgrade -u [-l ]" + "\n" + end + end +end + +def option_parse + options = {} + banner = "DIBS upgrade service command-line tool." + "\n" \ + + "\n" + "Usage: upgrade -u [-l ]" + "\n" \ + + "\n" + "Options:" + "\n" + + optparse = OptionParser.new(nil, 32, ' '*8) do|opts| + opts.banner = banner + + options[:locate] = File.dirname(__FILE__) + opts.on( '-l', '--locate ', 'located dibs path' ) do|locate| + options[:locate] = locate + end + + opts.on( '-u', '--url ', 'package server url: http://127.0.0.1/dibs/unstable' ) do|url| + options[:url] = url + end + + opts.on( '-I', '--install', 'install, internal option' ) do + options[:install] = true + end + + opts.on( '-S', '--start', 'start server option' ) do + options[:start] = true + end + + opts.on( '-t', '--type ', 'sever type : BUILDSERVER or PACKAGESERVER' ) do|type| + options[:type] = type + end + + opts.on( '-n', '--name ', 'build server name or package server name' ) do|name| + options[:name] = name + end + + opts.on( '-D', '--distribution ', 'build server distribution name' ) do|dist| + options[:dist] = dist + end + + options[:port] = 2222 + opts.on( '-p', '--port ', 'server port number: 2224' ) do|port| + options[:port] = port.strip.to_i + end + + opts.on( '-h', '--help', 'display this information' ) do + puts opts.banner + puts " -l, --locate , located dibs path" + puts " -u, --url , package server url: http://127.0.0.1/dibs/unstable" + exit + end + end + + optparse.parse! + + option_error_check options + + return options +end + +#option parsing +begin + option = option_parse +rescue => e + puts e.message + exit 0 +end + +# Upgrade DIBS +begin + install_opt = option[:install] + sub_cmd = option[:sub_cmd] + dibs_path = option[:locate] + pkg_svr_url= option[:url] + start_opt = option[:start] + svr_type = option[:type] + svr_name = option[:name] + svr_port = option[:port] + svr_dist = option[:dist] + + DIBS_PKG_NAME = "dibs" + BACKUP_ROOT = Utils::HOME + "/.build_tools/backup" + PREV_VER_PATH = BACKUP_ROOT + "/prev_ver" + NEW_VER_PATH = BACKUP_ROOT + "/new_ver" + UPGRADE_CMD = "#{PREV_VER_PATH}/upgrade" + BUILD_CONFIG_ROOT = "#{Utils::HOME}/.build_tools/build_server/#{svr_name}" + + if not svr_dist.nil? then + build_server = BuildServerController.get_server(svr_name) + dist = build_server.distmgr.get_distribution(svr_dist) + if dist.nil? then + log.error "Upgrade failed : No distribution name \"#{svr_dist}\" exist!", Log::LV_USER + cmd = Utils.generate_shell_command("#{dibs_path}/build-svr start -n #{svr_name} -p #{svr_port}") + Utils.spawn(cmd) + exit 1 + end + end + + if not File.exist? BACKUP_ROOT then FileUtils.mkdir_p(BACKUP_ROOT) end + log = StandardOutLog.new( "#{BUILD_CONFIG_ROOT}/log" ) + + if not install_opt then + puts "" + log.info("Upgrade Start...", Log::LV_USER) + + # Backup current dibs + if File.exist? PREV_VER_PATH then FileUtils.rm_rf(PREV_VER_PATH) end + if File.exist? NEW_VER_PATH then FileUtils.rm_rf(NEW_VER_PATH) end + FileUtils.mkdir_p(PREV_VER_PATH) + FileUtils.mkdir_p(NEW_VER_PATH) + FileUtils.cp_r("#{dibs_path}/.", PREV_VER_PATH, :preserve => true) + log.info("Backup DIBS [#{dibs_path}] -> [#{PREV_VER_PATH}]", Log::LV_USER) + + # Run Upgrade + if start_opt and svr_type.eql? "BUILDSERVER" then + cmd = "#{UPGRADE_CMD} -I -l #{dibs_path} -S -t #{svr_type} -n #{svr_name} -p #{svr_port}" + + if not (pkg_svr_url.nil? or pkg_svr_url.empty?) then + cmd += " -u #{pkg_svr_url}" + end + + if not (svr_dist.nil? or svr_dist.empty?) then + cmd += " -D #{svr_dist}" + end + + else + cmd = "#{UPGRADE_CMD} -I -l #{dibs_path} -u #{pkg_svr_url}" + end + + cmd = Utils.generate_shell_command(cmd) + Utils.spawn(cmd) + + else + # Get SERVER INFORMATION + if start_opt and svr_type.eql? "BUILDSERVER" then + # only when acesss build server controller + build_server = BuildServerController.get_server(svr_name) + if pkg_svr_url.nil? or pkg_svr_url.empty? then + if svr_dist.nil? or svr_dist.empty? then + pkg_svr_url = build_server.distmgr.get_default_pkgsvr_url() + else + dist = build_server.distmgr.get_distribution(svr_dist) + if not dist.nil? then + pkg_svr_url = dist.pkgsvr_url + else + log.error "Upgrade failed : No distribution name \"#{svr_dist}\" exist!", Log::LV_USER + exit 1 + end + end + end + log.info("Build server : [#{svr_name}][#{svr_port}]", Log::LV_USER) + end + log.info("Package Server : [#{pkg_svr_url}]", Log::LV_USER) + log.info("DIBS Path : [#{dibs_path}]", Log::LV_USER) + + # Download DIBS Package + client = Client.new( pkg_svr_url, NEW_VER_PATH, log) + client.update() + client.install( DIBS_PKG_NAME, Utils::HOST_OS, true, true) + + # Copy Current path + if File.exist? "#{dibs_path}" then + FileUtils.rm_rf("#{dibs_path}") + #FileUtils.mkdir_p("#{dibs_path}") + end + if File.exist? "#{NEW_VER_PATH}/tools/dibs" then + FileUtils.cp_r("#{NEW_VER_PATH}/tools/dibs/.", "#{dibs_path}", :preserve => true) + else + log.error("Not installed package error.", Log::LV_USER) + exit(1) + end + + # Execute start command + if start_opt + if not build_server.nil? and svr_type.eql? "BUILDSERVER" then + # get friends server information + build_server.get_remote_servers().each do |svr| + ip = svr.ip + port = svr.port + + build_client = BuildCommClient.create( ip, port ) + if build_client.nil? then + log.info("Friend Server #{ip}:#{port} is not running!", Log::LV_USER) + next + end + + # send request + log.info("Upgrading Friend Server #{ip}:#{port}...", Log::LV_USER) + if build_client.send "UPGRADE|#{build_server.password}" then + # recevie & print + mismatched = false + result = build_client.read_lines do |l| + log.info(l, Log::LV_USER) + if l.include? "Password mismatched!" then + mismatched = true + end + end + if not result then + log.info("Upgrading failed! #{build_client.get_error_msg()}", Log::LV_USER) + elsif mismatched then + log.info("Upgrading failed! Password mismatched!", Log::LV_USER) + end + else + log.info("Upgrading failed! #{build_client.get_error_msg()}", Log::LV_USER) + next + end + + # terminate + build_client.terminate + end + + # Start Build server + cmd = Utils.generate_shell_command("#{dibs_path}/build-svr start -n #{svr_name} -p #{svr_port}") + Utils.spawn(cmd) + + log.info("Upgrade Complete", Log::LV_USER) + log.info("Start Build server [#{cmd}]", Log::LV_USER) + + else # PACKAGE SERVER + # Start Build server + cmd = Utils.generate_shell_command("#{dibs_path}/pkg-svr start -n #{svr_name} -p #{svr_port}") + Utils.spawn(cmd) + + log.info("Upgrade Complete", Log::LV_USER) + log.info("Start Package server [#{cmd}]", Log::LV_USER) + end + else + log.info("Upgrade Complete", Log::LV_USER) + end + end +rescue => e + log.error(e.message, Log::LV_USER) + #puts e.message +end + -- 2.7.4