├── crontab_create.png ├── crontab_list.png ├── crontab_log_list.png ├── crontab_log_detail.png ├── .gitignore ├── routes └── web.php ├── src ├── Http │ ├── Models │ │ ├── Crontab.php │ │ └── CrontabLog.php │ └── Controllers │ │ ├── CrontabLogController.php │ │ └── CrontabController.php ├── Crontab.php ├── CrontabServiceProvider.php └── autoTask.php ├── composer.json ├── LICENSE ├── README.md └── migrations └── 2019_03_18_083704_create_crontab_table.php /crontab_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArrowJustDoIt/crontab/HEAD/crontab_create.png -------------------------------------------------------------------------------- /crontab_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArrowJustDoIt/crontab/HEAD/crontab_list.png -------------------------------------------------------------------------------- /crontab_log_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArrowJustDoIt/crontab/HEAD/crontab_log_list.png -------------------------------------------------------------------------------- /crontab_log_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArrowJustDoIt/crontab/HEAD/crontab_log_detail.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | phpunit.phar 3 | /vendor 4 | composer.phar 5 | composer.lock 6 | *.project 7 | .idea/ -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | hasMany(CrontabLog::class, 'cid', 'id'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Crontab.php: -------------------------------------------------------------------------------- 1 | '定时任务', 15 | 'path' => 'crontab', 16 | 'icon' => 'fa-gears', 17 | ]; 18 | } -------------------------------------------------------------------------------- /src/Http/Models/CrontabLog.php: -------------------------------------------------------------------------------- 1 | belongsTo(Crontab::class, 'cid', 'id'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CrontabServiceProvider.php: -------------------------------------------------------------------------------- 1 | migrations()) { 21 | $this->loadMigrationsFrom($migrations); 22 | } 23 | 24 | //命令 25 | if ($this->app->runningInConsole()) { 26 | $this->commands([ 27 | autoTask::class, 28 | ]); 29 | } 30 | 31 | $this->app->booted(function () { 32 | //路由 33 | Crontab::routes(__DIR__.'/../routes/web.php'); 34 | }); 35 | } 36 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arrowjustdoit/crontab", 3 | "description": "Crontab extension for laravel-admin", 4 | "type": "library", 5 | "keywords": ["laravel-admin", "extension", "crontab"], 6 | "homepage": "https://github.com/ArrowJustDoIt/crontab", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "arrow", 11 | "email": "arrowylq@outlook.com" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=7.0.0", 16 | "encore/laravel-admin": "~1.6", 17 | "guzzlehttp/guzzle": "^6.3" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~6.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "ArrowJustDoIt\\Crontab\\": "src/" 25 | } 26 | }, 27 | "extra": { 28 | "laravel": { 29 | "providers": [ 30 | "ArrowJustDoIt\\Crontab\\CrontabServiceProvider" 31 | ] 32 | 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jens Segers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Crontab extension for laravel-admin 2 | ====== 3 | 4 | [Crontab](https://github.com/ArrowJustDoIt/Crontab)是一个laravel-admin后台的定时任务扩展插件,你可以通过此插件定时执行shell、sql以及访问指定链接 5 | 6 | [dcat-admin版本](https://github.com/ArrowJustDoIt/dcat-admin-crontab-extension) 7 | 8 | ## 截图 9 | ![crontab列表](https://raw.githubusercontent.com/ArrowJustDoIt/crontab/master/crontab_list.png) 10 | 11 | ![crontab创建](https://raw.githubusercontent.com/ArrowJustDoIt/crontab/master/crontab_create.png) 12 | 13 | ![crontablog列表](https://raw.githubusercontent.com/ArrowJustDoIt/crontab/master/crontab_log_list.png) 14 | 15 | ![crontablog详情](https://raw.githubusercontent.com/ArrowJustDoIt/crontab/master/crontab_log_detail.png) 16 | ## 安装 17 | 18 | ```bash 19 | composer require arrowjustdoit/crontab 20 | php artisan migrate 21 | ``` 22 | 23 | ## 配置 24 | 25 | 在`config/admin.php`文件的`extensions`配置部分,加上属于这个扩展的配置 26 | ```php 27 | 28 | 'extensions' => [ 29 | 30 | 'crontab' => [ 31 | 32 | // 如果要关掉这个扩展,设置为false 33 | 'enable' => true, 34 | ] 35 | ] 36 | 37 | ``` 38 | 39 | 在服务器中配置crontab 40 | 41 | ``` 42 | crontab -e //回车 43 | * * * * * php /your web dir/artisan autotask:run >>/home/crontab.log 2>&1 //>>后面为日志文件保存地址,可加可不加 44 | ``` 45 | 46 | ## 访问 47 | 48 | ``` 49 | https://your domain/admin/crontabs #定时任务列表 50 | https://your domain/admin/crontabLogs #定时任务日志列表 51 | ``` 52 | 53 | 54 | ## License 55 | 56 | Licensed under [The MIT License (MIT)](LICENSE). 57 | -------------------------------------------------------------------------------- /migrations/2019_03_18_083704_create_crontab_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('type',10)->comment('类型'); 19 | $table->string('title',150)->comment('标题'); 20 | $table->text('contents')->comment('内容'); 21 | $table->string('schedule',100)->comment('Cron表达式'); 22 | $table->tinyInteger('sleep')->default(0)->comment('延迟秒数执行'); 23 | $table->integer('maximums')->default(0)->comment('最大执行次数 0为不限'); 24 | $table->integer('executes')->default(0)->nullable()->comment('已经执行的次数'); 25 | $table->dateTime('begin_at')->comment('开始时间'); 26 | $table->dateTime('end_at')->comment('结束时间'); 27 | $table->dateTime('execute_at')->nullable()->comment('最后执行时间'); 28 | $table->integer('weigh')->default(0)->comment('权重'); 29 | $table->enum('status',['completed','expired','disable','normal'])->default('normal')->comment('状态'); 30 | $table->timestamps(); 31 | }); 32 | Schema::create('crontab_log', function (Blueprint $table) { 33 | $table->increments('id'); 34 | $table->string('type',10)->comment('类型'); 35 | $table->integer('cid')->comment('任务的ID'); 36 | $table->string('title',150)->comment('标题'); 37 | $table->mediumText('remark')->comment('备注'); 38 | $table->tinyInteger('status')->comment('状态 0:失败 1:成功'); 39 | $table->timestamps(); 40 | }); 41 | } 42 | 43 | /** 44 | * Reverse the migrations. 45 | * 46 | * @return void 47 | */ 48 | public function down() 49 | { 50 | Schema::dropIfExists('crontab'); 51 | Schema::dropIfExists('crontab_log'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Http/Controllers/CrontabLogController.php: -------------------------------------------------------------------------------- 1 | breadcrumb( 26 | ['text' => '定时任务日志', 'url' => '/crontabLogs'], 27 | ['text' => '列表'] 28 | ); 29 | return $content 30 | ->header('列表') 31 | ->description('定时任务日志') 32 | ->body($this->grid()); 33 | } 34 | 35 | /** 36 | * Show interface. 37 | * 38 | * @param mixed $id 39 | * @param Content $content 40 | * @return Content 41 | */ 42 | public function show($id, Content $content) 43 | { 44 | $content->breadcrumb( 45 | ['text' => '定时任务日志', 'url' => '/crontabLogs'], 46 | ['text' => '详情'] 47 | ); 48 | return $content 49 | ->header('详情') 50 | ->description('定时任务日志') 51 | ->body($this->detail($id)); 52 | } 53 | 54 | /** 55 | * Make a grid builder. 56 | * 57 | * @return Grid 58 | */ 59 | protected function grid() 60 | { 61 | $grid = new Grid(new CrontabLog()); 62 | $grid->disableCreateButton(); 63 | $grid->id('Id')->sortable(); 64 | $grid->type('类型')->using(CrontabController::CRONTAB_TYPE)->label('default'); 65 | $grid->title('任务标题'); 66 | $grid->created_at('执行时间'); 67 | $grid->status('状态')->sortable()->using(['0'=>'失败','1'=>'成功'])->display(function ($status) { 68 | if($status == '失败'){ 69 | return ''.$status.''; 70 | }else{ 71 | return ''.$status.''; 72 | } 73 | }); 74 | $grid->actions(function ($actions) { 75 | $actions->disableEdit(); 76 | }); 77 | $grid->filter(function($filter){ 78 | $filter->disableIdFilter(); 79 | $filter->like('title', '任务标题'); 80 | $filter->equal('type', '类型')->select(CrontabController::CRONTAB_TYPE); 81 | 82 | }); 83 | return $grid; 84 | } 85 | 86 | 87 | /** 88 | * Make a show builder. 89 | * 90 | * @param mixed $id 91 | * @return Show 92 | */ 93 | protected function detail($id) 94 | { 95 | $show = new Show(CrontabLog::findOrFail($id)); 96 | 97 | $show->type('类型')->using(CrontabController::CRONTAB_TYPE)->label(); 98 | $show->cid('任务ID'); 99 | $show->title('任务标题'); 100 | $show->created_at('执行时间'); 101 | $show->status('状态')->using([0 => '失败',1 => '成功']); 102 | $show->remark('执行结果'); 103 | 104 | $show->panel()->tools(function ($tools) { 105 | $tools->disableEdit(); 106 | }); 107 | 108 | return $show; 109 | } 110 | 111 | /** 112 | * Make a form builder. 113 | * 114 | * @return Form 115 | */ 116 | protected function form() 117 | { 118 | $form = new Form(new CrontabLog); 119 | return $form; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/autoTask.php: -------------------------------------------------------------------------------- 1 | 'normal'])->orderBy('weigh','desc')->orderBy('id','desc')->get(); 50 | if (!$crontab_list) { 51 | return null; 52 | } 53 | $time = time(); 54 | foreach ($crontab_list as $key => $crontab) { 55 | $value = $crontab->toArray(); 56 | $execute = false; // 是否执行 57 | 58 | if ($time < strtotime($value['begin_at'])) { //任务未开始 59 | continue; 60 | } 61 | 62 | if ($value['maximums'] && $value['executes'] > $value['maximums']) { //任务已超过最大执行次数 63 | $crontab->status = 'completed'; 64 | } else if (strtotime($value['end_at']) > 0 && $time > strtotime($value['end_at'])) { //任务已过期 65 | $crontab->status = 'expired'; 66 | } else { 67 | $cron = CronExpression::factory($value['schedule']); 68 | /* 69 | * 根据当前时间判断是否该应该执行 70 | * 这个判断和秒数无关,其最小单位为分 71 | * 也就是说,如果处于该执行的这个分钟内如果多次调用都会判定为真 72 | * 所以我们在服务器上设置的定时任务最小单位应该是分 73 | */ 74 | if ($cron->isDue()) { 75 | // 允许执行 76 | $execute = true; 77 | // 允许执行的时候更新状态 78 | $crontab->execute_at = date('Y-m-d H:i:s'); 79 | $crontab->executes = $value['executes'] + 1; 80 | $crontab->status = ($value['maximums'] > 0 && $crontab->executes >= $value['maximums']) ? 'completed' : 'normal'; 81 | } else { //如果未到执行时间则跳过本任务去判断下一个任务 82 | continue; 83 | } 84 | } 85 | // 更新状态 86 | $crontab->save(); 87 | // 如果不允许执行,只是从当前开始已过期或者已超过最大执行次数的任务,只是更新状态就行了,不执行 88 | if (!$execute) { 89 | continue; 90 | } 91 | 92 | try { 93 | // 分类执行任务 94 | switch ($value['type']) { 95 | case 'url': 96 | try { 97 | $client = new Client(); 98 | $response = $client->request('GET', $value['contents']); 99 | $this->saveLog('url', $value['id'], $value['title'], 1, $value['contents'] . ' 请求成功,HTTP状态码: ' . $response->getStatusCode()); 100 | } catch (RequestException $e) { 101 | $this->saveLog('url', $value['id'], $value['title'], 0, $value['contents'] . ' 请求成功失败: ' . $e->getMessage()); 102 | } 103 | break; 104 | case 'sql': 105 | /* 注释中的方法可以一次执行所有SQL语句 106 | // 执行SQL 107 | $count = DB::select($crontab['contents']); 108 | dump($count );*/ 109 | 110 | // 解析成一条条的sql语句 111 | $sqls = str_replace("\r", "\n", $value['contents']); 112 | $sqls = explode(";\n", $sqls); 113 | $remark = ''; 114 | $status = 1; 115 | foreach ($sqls as $sql) { 116 | $sql = trim($sql); 117 | if (empty($sql)) continue; 118 | if (substr($sql, 0, 2) == '--') continue; // SQL注释 119 | // 执行SQL并记录执行结果 120 | if (false !== DB::select($sql)) { 121 | $remark .= '执行成功: ' . $sql . "\r\n\r\n"; 122 | } else { 123 | $remark .= '执行失败: ' . $sql . "\r\n\r\n"; 124 | $status = 0; 125 | } 126 | } 127 | $this->saveLog('sql', $value['id'], $value['title'], $status, $remark); 128 | break; 129 | case 'shell': 130 | $status = 0; 131 | $request = 'fail'; 132 | 133 | $process = new Process($value['contents']); 134 | $process->run(); 135 | if ($process->isSuccessful()) { 136 | $status = 1; 137 | $request = $process->getOutput(); 138 | } 139 | $this->saveLog('shell', $value['id'], $value['title'], $status, $request); 140 | break; 141 | } 142 | } 143 | catch (Exception $e) 144 | { 145 | $this->saveLog($value['type'], $value['id'], $value['title'], 0, "执行的内容发生异常:\r\n" . $e->getMessage()); 146 | } 147 | 148 | print_r('执行完毕'); 149 | } 150 | } 151 | 152 | // 保存运行日志 153 | private function saveLog($type, $cid, $title, $status, $remark = '') 154 | { 155 | $crontLog = new CrontabLog(); 156 | $crontLog->type = $type; 157 | $crontLog->cid = $cid; 158 | $crontLog->title = $title; 159 | $crontLog->status = $status; 160 | $crontLog->remark = $remark; 161 | $crontLog->save(); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Http/Controllers/CrontabController.php: -------------------------------------------------------------------------------- 1 | '执行sql', 21 | 'shell'=>'执行shell', 22 | 'url'=>'请求url' 23 | ]; 24 | const CRONTAB_STATUS = [ 25 | 'normal'=>'正常', 26 | 'disable'=>'禁用', 27 | 'completed'=>'完成', 28 | 'expired'=>'过期' 29 | ]; 30 | 31 | /** 32 | * Index interface. 33 | * 34 | * @param Content $content 35 | * @return Content 36 | */ 37 | public function index(Content $content) 38 | { 39 | $content->breadcrumb( 40 | ['text' => '定时任务', 'url' => '/crontabs'], 41 | ['text' => '列表'] 42 | ); 43 | return $content 44 | ->header('列表') 45 | ->description('定时任务') 46 | ->body($this->grid()); 47 | } 48 | 49 | /** 50 | * Edit interface. 51 | * 52 | * @param mixed $id 53 | * @param Content $content 54 | * @return Content 55 | */ 56 | public function edit($id, Content $content) 57 | { 58 | $content->breadcrumb( 59 | ['text' => '定时任务', 'url' => '/crontabs'], 60 | ['text' => '编辑'] 61 | ); 62 | return $content 63 | ->header('编辑') 64 | ->description('定时任务') 65 | ->body($this->form()->edit($id)); 66 | } 67 | 68 | /** 69 | * Create interface. 70 | * 71 | * @param Content $content 72 | * @return Content 73 | */ 74 | public function create(Content $content) 75 | { 76 | $content->breadcrumb( 77 | ['text' => '定时任务', 'url' => '/crontabs'], 78 | ['text' => '创建'] 79 | ); 80 | return $content 81 | ->header('创建') 82 | ->description('定时任务') 83 | ->body($this->form()); 84 | } 85 | 86 | /** 87 | * Make a grid builder. 88 | * 89 | * @return Grid 90 | */ 91 | protected function grid() 92 | { 93 | $grid = new Grid(new Crontab); 94 | $grid->id('Id')->sortable(); 95 | $grid->type('类型')->using(self::CRONTAB_TYPE)->label('default'); 96 | $grid->title('任务标题'); 97 | $grid->maximums('最大次数'); 98 | $grid->executes('已执行次数')->sortable(); 99 | $grid->execute_at('下次预计时间'); 100 | $grid->end_at('最后执行时间')->sortable(); 101 | $grid->status('状态')->sortable()->using(self::CRONTAB_STATUS)->display(function ($status) { 102 | switch ($status){ 103 | case '正常': 104 | return ''.$status.''; 105 | break; 106 | case '禁用': 107 | return ''.$status.''; 108 | break; 109 | case '完成': 110 | return ''.$status.''; 111 | break; 112 | default : 113 | return ''.$status.''; 114 | break; 115 | } 116 | }); 117 | $grid->created_at('创建时间'); 118 | $grid->actions(function ($actions) { 119 | $actions->disableView(); 120 | }); 121 | $grid->filter(function($filter){ 122 | $filter->disableIdFilter(); 123 | $filter->like('title', '任务标题'); 124 | $filter->equal('type', '类型')->select(self::CRONTAB_TYPE); 125 | 126 | }); 127 | 128 | return $grid; 129 | } 130 | 131 | 132 | /** 133 | * Make a form builder. 134 | * 135 | * @return Form 136 | */ 137 | protected function form() 138 | { 139 | $form = new Form(new Crontab); 140 | 141 | $form->text('title', '任务标题')->rules('required',['required'=>'任务标题不能为空']); 142 | $form->select('type','任务类型')->options(self::CRONTAB_TYPE)->help("1. URL类型是完整的URL地址,如: http://www.baidu.com/
2. 如果你的服务器 php.ini 未开启 shell_exec() 函数,则不能使用URL类型和Shell类型模式!")->rules('required|in:url,sql,shell',['required'=>'任务类型不能为空','in'=>'参数错误']); 143 | $form->textarea('contents', '内容')->rows(3)->rules('required',['required'=>'内容不能为空']); 144 | $form->text('schedule', '执行周期')->default('* * * * *')->help("请使用Cron表达式")->rules(function ($form) { 145 | $value = $form->model()->schedule; 146 | if (empty($value)){ 147 | return 'required'; 148 | } 149 | if (!CronExpression::isValidExpression($value)){ 150 | // return 'max:0'; 151 | } 152 | },['required'=>'执行周期不能为空','max'=>'执行周期 Cron 表达式错误']); 153 | 154 | $form->html("
*    *    *    *    *
155 | -    -    -    -    -
156 | |    |    |    |    +--- day of week (0 - 7) (Sunday=0 or 7)
157 | |    |    |    +-------- month (1 - 12)
158 | |    |    +------------- day of month (1 - 31)
159 | |    +------------------ hour (0 - 23)
160 | +----------------------- min (0 - 59)
"); 161 | $checkSchedule_url = url('admin/crontabs/checkSchedule'); 162 | $js = << 164 | function checkSchedule() { 165 | var schedule = $("#schedule").val(); 166 | $.post("{$checkSchedule_url}", {"schedule":schedule,_token:LA.token}, 167 | function(data){ 168 | if (data.status == false){ 169 | toastr.error(data.message); 170 | return false; 171 | } 172 | }, "json"); 173 | } 174 | 175 | $(function(){ 176 | checkSchedule(); // 页面加载后就执行一次 177 | // 检查 Cron 表达式是否正确,如果正确,则获取预计执行时间 178 | $("#schedule,#begin_at").blur(function(){ 179 | checkSchedule(); 180 | }); 181 | }); 182 | 183 | EOF; 184 | $form->html($js); 185 | 186 | $form->number('maximums', '最大执行次数')->default(0)->help("0为不限次数")->rules('required|integer|min:0',[ 187 | 'required'=>'最大执行次数不能为空', 188 | 'integer'=>'最大执行次数必须为正整数', 189 | 'min'=>'最大执行次数不能为负数', 190 | ]); 191 | $form->number('executes', '已执行次数')->default(0)->help("如果任务执行次数达到上限,则会自动把状态改为“完成” 192 | 如果已“完成”的任务需要再次运行,请重置本参数或者调整最大执行次数并把下面状态值改成“正常”"); 193 | $form->datetime('begin_at', '开始时间')->default(date('Y-m-d H:i:s'))->help("如果设置了开始时间,则从开始时间计算;
如果没有设置开始时间,则以当前时间计算。")->rules('required|date',['required'=>'开始时间不能为空','date'=>'时间格式不正确']); 194 | $form->datetime('end_at', '结束时间')->default(date('Y-m-d H:i:s'))->help("如果需要长期执行,请把结束时间设置得尽可能的久")->rules('required|date',['required'=>'结束时间不能为空','date'=>'时间格式不正确']); 195 | $form->number('weigh', '权重')->default(100)->help("多个任务同一时间执行时,按照权重从高到底执行")->rules('required|integer',['required'=>'权重不能为空','integer'=>'权重必须为正整数']); 196 | $form->select('status', '状态')->default('normal')->options(self::CRONTAB_STATUS)->rules('required|in:disable,normal,completed,expired',['required'=>'状态不能为空','in'=>'参数错误']); 197 | 198 | $form->tools(function (Form\Tools $tools) { 199 | // 去掉`查看`按钮 200 | $tools->disableView(); 201 | }); 202 | 203 | return $form; 204 | } 205 | 206 | /** 207 | * validate schedule. 208 | * 209 | * @return json 210 | */ 211 | public function checkSchedule(Request $request){ 212 | $schedule = $request->post('schedule',''); 213 | if (empty($schedule)){ 214 | return Response::json(['status'=>false,'message'=>'执行周期不能为空']); 215 | } 216 | if (!CronExpression::isValidExpression($schedule)){ 217 | return Response::json(['status'=>false,'message'=>'执行周期 Cron 表达式错误']); 218 | } 219 | return Response::json(['status'=>true,'message'=>'']); 220 | } 221 | } 222 | --------------------------------------------------------------------------------